macos.rb 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +572 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +21 -0
  5. data/README.md +129 -0
  6. data/Rakefile +9 -0
  7. data/lib/Array/all_but_last.rb +7 -0
  8. data/lib/MacOS/Apple80211.rb +68 -0
  9. data/lib/MacOS/Applications.rb +73 -0
  10. data/lib/MacOS/Defaults/ComAppleDock.rb +153 -0
  11. data/lib/MacOS/Defaults/NSGlobalDomain/NSUserKeyEquivalents.rb +109 -0
  12. data/lib/MacOS/Defaults.rb +126 -0
  13. data/lib/MacOS/DiskUtil.rb +63 -0
  14. data/lib/MacOS/Dmg.rb +99 -0
  15. data/lib/MacOS/HardwarePort.rb +79 -0
  16. data/lib/MacOS/IfConfig.rb +183 -0
  17. data/lib/MacOS/Keychain.rb +26 -0
  18. data/lib/MacOS/LaunchControl.rb +36 -0
  19. data/lib/MacOS/SystemProfiler.rb +119 -0
  20. data/lib/MacOS/USBFlashDrive.rb +26 -0
  21. data/lib/MacOS/Users.rb +16 -0
  22. data/lib/MacOS/VERSION.rb +6 -0
  23. data/lib/MacOS/VPN.rb +13 -0
  24. data/lib/MacOS/Voice.rb +40 -0
  25. data/lib/MacOS.rb +27 -0
  26. data/lib/Module/alias_methods.rb +7 -0
  27. data/lib/Ordinal/Array.rb +8 -0
  28. data/lib/Ordinal.rb +157 -0
  29. data/lib/Thoran/Array/AllButLast/all_but_last.rb +28 -0
  30. data/lib/Thoran/Array/LastX/lastX.rb +33 -0
  31. data/lib/Thoran/Module/AliasMethods/alias_methods.rb +23 -0
  32. data/lib/Thoran/String/Capture/capture.rb +28 -0
  33. data/lib/Version.rb +381 -0
  34. data/macos.rb.gemspec +59 -0
  35. data/test/MacOS/Apple80211_test.rb +80 -0
  36. data/test/MacOS/Applications_test.rb +82 -0
  37. data/test/MacOS/Defaults/ComAppleDock_test.rb +66 -0
  38. data/test/MacOS/Defaults/NSGlobalDomain/NSUserKeyEquivalents_test.rb +67 -0
  39. data/test/MacOS/Defaults_test.rb +144 -0
  40. data/test/MacOS/DiskUtil_test.rb +48 -0
  41. data/test/MacOS/Dmg_test.rb +69 -0
  42. data/test/MacOS/HardwarePort_test.rb +46 -0
  43. data/test/MacOS/IfConfig_test.rb +144 -0
  44. data/test/MacOS/Keychain_test.rb +22 -0
  45. data/test/MacOS/LaunchControl_test.rb +40 -0
  46. data/test/MacOS/SystemProfiler_test.rb +69 -0
  47. data/test/MacOS/USBFlashDrive_test.rb +20 -0
  48. data/test/MacOS/Users_test.rb +10 -0
  49. data/test/MacOS/VPN_test.rb +18 -0
  50. data/test/MacOS/Voice_test.rb +46 -0
  51. data/test/MacOS_test.rb +17 -0
  52. data/test/VERSION_test.rb +19 -0
  53. data/test/commands_test.rb +50 -0
  54. data/test/fixtures/airport-I-deprecated.txt +2 -0
  55. data/test/fixtures/airport-I.txt +17 -0
  56. data/test/fixtures/defaults-domains.txt +1 -0
  57. data/test/fixtures/defaults-export-com.apple.dock.xml +47 -0
  58. data/test/fixtures/defaults-read-NSUserKeyEquivalents.txt +4 -0
  59. data/test/fixtures/df-k.txt +6 -0
  60. data/test/fixtures/diskutil-list-plist.xml +104 -0
  61. data/test/fixtures/dscl-list-users.txt +8 -0
  62. data/test/fixtures/ifconfig-a.txt +53 -0
  63. data/test/fixtures/networksetup-listallhardwareports.txt +39 -0
  64. data/test/fixtures/say-v.txt +8 -0
  65. data/test/fixtures/system_profiler-SPAirPortDataType.txt +44 -0
  66. data/test/fixtures/system_profiler-SPSoftwareDataType.txt +14 -0
  67. data/test/fixtures/system_profiler-SPUSBDataType.txt +58 -0
  68. data/test/gemspec_test.rb +50 -0
  69. data/test/helper.rb +18 -0
  70. metadata +183 -0
@@ -0,0 +1,67 @@
1
+ require_relative '../../../helper'
2
+ require 'MacOS/Defaults/NSGlobalDomain/NSUserKeyEquivalents'
3
+
4
+ describe MacOS::Defaults::NSGlobalDomain::NSUserKeyEquivalents do
5
+ let(:keys){MacOS::Defaults::NSGlobalDomain::NSUserKeyEquivalents}
6
+
7
+ describe ".key_code" do
8
+ it "codes modifier and arrow keys, in either case" do
9
+ _(keys.key_code(%w{cmd alt left})).must_equal('@~\\UF702')
10
+ _(keys.key_code(%w{CMD ALT LEFT})).must_equal('@~\\UF702')
11
+ _(keys.key_code(:ctrl, :shift, :right)).must_equal('^$\\UF703')
12
+ end
13
+
14
+ it "keeps a letter as itself" do
15
+ _(keys.key_code(%w{cmd D})).must_equal('@d')
16
+ end
17
+
18
+ it "raises for a key it doesn't know" do
19
+ _{keys.key_code(%w{cmd hyper})}.must_raise(RuntimeError)
20
+ end
21
+ end
22
+
23
+ describe ".parse_key_code" do
24
+ it "splits a key code at each \\U" do
25
+ _(keys.parse_key_code('@~\UF702')).must_equal(['@~', 'F702'])
26
+ end
27
+ end
28
+
29
+ describe "assigning keys" do
30
+ it "makes a defaults write command for each menu item" do
31
+ _(keys.assign_key_code_command_strings('@~\\UF702', ['Previous Tab', 'Show Previous Tab'])).must_equal([
32
+ "defaults write NSGlobalDomain NSUserKeyEquivalents -dict-add 'Previous Tab' '@~\\UF702'",
33
+ "defaults write NSGlobalDomain NSUserKeyEquivalents -dict-add 'Show Previous Tab' '@~\\UF702'",
34
+ ])
35
+ end
36
+
37
+ it "runs them, succeeding where each does" do
38
+ result = nil
39
+ commands = commands_run_by(keys){result = keys.assign_keys(%w{cmd alt left}, 'Previous Tab')}
40
+ _(result).must_equal(true)
41
+ _(commands).must_equal([["defaults write NSGlobalDomain NSUserKeyEquivalents -dict-add 'Previous Tab' '@~\\UF702'"]])
42
+ end
43
+
44
+ it "loads several shortcuts at once" do
45
+ shortcuts = [{keys: %w{cmd alt left}, menu_items: ['Previous Tab']}, {keys: %w{cmd shift d}, menu_items: ['Find Previous']}]
46
+ commands = commands_run_by(keys){keys.load(shortcuts)}
47
+ _(commands.size).must_equal(2)
48
+ end
49
+ end
50
+
51
+ describe ".read_keys" do
52
+ it "reads the shortcuts back, as keys and the menu items they're for" do
53
+ keys.stub(:`, fixture('defaults-read-NSUserKeyEquivalents.txt')) do
54
+ _(keys.read_keys).must_equal([
55
+ {keys: [:cmd, :alt, :left], menu_items: ['Previous Tab']},
56
+ {keys: [:cmd, :shift, 'd'], menu_items: ['Find Previous']},
57
+ ])
58
+ end
59
+ end
60
+
61
+ it "writes them as YAML" do
62
+ keys.stub(:`, fixture('defaults-read-NSUserKeyEquivalents.txt')) do
63
+ _(YAML.load(keys.to_yaml, permitted_classes: [Symbol]).first[:menu_items]).must_equal(['Previous Tab'])
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,144 @@
1
+ require_relative '../helper'
2
+ require 'tmpdir'
3
+ require 'MacOS/Defaults'
4
+
5
+ describe MacOS::Defaults do
6
+ def with_domains(&block)
7
+ MacOS::Defaults.stub(:`, fixture('defaults-domains.txt'), &block)
8
+ end
9
+
10
+ # Stands in for MacOS::Defaults.new, making Defaults whose named methods only record the calls made on them.
11
+ def recorder(calls, *methods)
12
+ ->(domain:) do
13
+ defaults = MacOS::Defaults.allocate
14
+ defaults.send(:initialize, domain: domain)
15
+ methods.each{|method| defaults.define_singleton_method(method){|*args| calls << [domain, method, *args]}}
16
+ defaults
17
+ end
18
+ end
19
+
20
+ describe ".domains" do
21
+ it "lists every domain" do
22
+ with_domains do
23
+ _(MacOS::Defaults.domains).must_equal(%w{com.apple.dock com.apple.finder com.apple.Safari})
24
+ end
25
+ end
26
+
27
+ it "makes a Defaults for each" do
28
+ with_domains do
29
+ _(MacOS::Defaults.all.map{|defaults| defaults.instance_variable_get(:@domain)}).must_equal(%w{com.apple.dock com.apple.finder com.apple.Safari})
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "a domain" do
35
+ it "is valid where defaults lists it, or where it's the global domain" do
36
+ with_domains do
37
+ _(MacOS::Defaults.new(domain: 'com.apple.dock').domain_valid?).must_equal(true)
38
+ _(MacOS::Defaults.new(domain: '-globalDomain').domain_valid?).must_equal(true)
39
+ _(MacOS::Defaults.new(domain: 'com.example.nowhere').domain_valid?).must_equal(false)
40
+ end
41
+ end
42
+
43
+ it "knows whether it's the global domain" do
44
+ _(MacOS::Defaults.new(domain: '-globalDomain').global_domain?).must_equal(true)
45
+ _(MacOS::Defaults.new(domain: 'com.apple.dock').global_domain?).must_equal(false)
46
+ end
47
+ end
48
+
49
+ describe "exporting and importing" do
50
+ it "exports a valid domain to a plist file" do
51
+ commands = []
52
+ MacOS::Defaults.stub(:domains, %w{com.apple.dock}) do
53
+ defaults = MacOS::Defaults.new(domain: 'com.apple.dock')
54
+ defaults.stub(:`, ->(command){commands << command; ''}){_(defaults.export).must_equal(true)}
55
+ end
56
+ _(commands).must_equal(['defaults export com.apple.dock /tmp/com.apple.dock.plist'])
57
+ end
58
+
59
+ it "won't export a domain defaults doesn't know" do
60
+ MacOS::Defaults.stub(:domains, []) do
61
+ _{MacOS::Defaults.new(domain: 'com.example.nowhere').export}.must_raise(RuntimeError)
62
+ end
63
+ end
64
+
65
+ it "won't import a domain without its plist file" do
66
+ _{MacOS::Defaults.new(domain: 'com.example.nowhere').import}.must_raise(RuntimeError)
67
+ end
68
+
69
+ it "exports and imports every domain in bulk" do
70
+ calls = []
71
+ MacOS::Defaults.stub(:domains, %w{com.apple.dock com.apple.finder}) do
72
+ MacOS::Defaults.stub(:new, recorder(calls, :export, :import)) do
73
+ MacOS::Defaults.bulk_export_to_plist_files
74
+ MacOS::Defaults.bulk_import_from_plist_files
75
+ end
76
+ end
77
+ _(calls).must_equal([['com.apple.dock', :export], ['com.apple.finder', :export], ['com.apple.dock', :import], ['com.apple.finder', :import]])
78
+ end
79
+ end
80
+
81
+ describe "from YAML" do
82
+ let(:attributes){[{'autohide' => true}, {'tilesize' => 48}, {'scale' => 0.5}, {'orientation' => 'bottom'}, {'persistent-apps' => ['/Applications/Example.app']}]}
83
+
84
+ it "writes each attribute, typed by its value" do
85
+ defaults = MacOS::Defaults.new(domain: 'com.apple.dock')
86
+ commands = commands_run_by(defaults){_(defaults.from_yaml(attributes)).must_equal(true)}
87
+ _(commands).must_equal([
88
+ %w{defaults write com.apple.dock autohide -bool true},
89
+ %w{defaults write com.apple.dock tilesize -int 48},
90
+ %w{defaults write com.apple.dock scale -float 0.5},
91
+ %w{defaults write com.apple.dock orientation -string bottom},
92
+ %w{defaults write com.apple.dock persistent-apps -array /Applications/Example.app},
93
+ ])
94
+ end
95
+
96
+ it "is false where a write fails" do
97
+ defaults = MacOS::Defaults.new(domain: 'com.apple.dock')
98
+ defaults.stub(:system, false) do
99
+ _(defaults.from_yaml([{'autohide' => true}])).must_equal(false)
100
+ end
101
+ end
102
+
103
+ it "writes a Hash as a dictionary" do
104
+ defaults = MacOS::Defaults.new(domain: 'com.apple.dock')
105
+ commands = commands_run_by(defaults){defaults.from_yaml([{'example-dict' => {'size' => 48, 'shown' => true}}])}
106
+ _(commands).must_equal([%w{defaults write com.apple.dock example-dict -dict size 48 shown true}])
107
+ end
108
+
109
+ it "reads what to_yaml writes" do
110
+ written = MacOS::Defaults.new(domain: 'com.apple.dock')
111
+ yaml = written.stub(:to_h, {'autohide' => true, 'tilesize' => 48}){written.to_yaml}
112
+ _(YAML.safe_load(yaml)['com.apple.dock']).must_equal([{'autohide' => true}, {'tilesize' => 48}])
113
+ end
114
+
115
+ it "writes each domain a YAML file gives" do
116
+ Dir.mktmpdir do |directory|
117
+ path = File.join(directory, 'defaults.yml')
118
+ File.write(path, {'com.apple.dock' => [{'autohide' => true}], 'com.apple.finder' => [{'ShowPathbar' => true}]}.to_yaml)
119
+ calls = []
120
+ MacOS::Defaults.stub(:new, recorder(calls, :from_yaml)){MacOS::Defaults.from_yaml(path)}
121
+ _(calls).must_equal([['com.apple.dock', :from_yaml, [{'autohide' => true}]], ['com.apple.finder', :from_yaml, [{'ShowPathbar' => true}]]])
122
+ end
123
+ end
124
+
125
+ it "reads DEFAULT_YAML_FILENAME where it's given no file" do
126
+ loaded = []
127
+ YAML.stub(:load_file, ->(path){loaded << path; {}}){MacOS::Defaults.from_yaml}
128
+ _(loaded).must_equal([MacOS::Defaults::DEFAULT_YAML_FILENAME])
129
+ end
130
+ end
131
+
132
+ describe "#to_h" do
133
+ it "reads the domain's exported plist file" do
134
+ domain = "com.example.macos-rb-test-#{Process.pid}"
135
+ path = File.join(MacOS::Defaults::DEFAULT_PLIST_DIRECTORY, "#{domain}.plist")
136
+ plist = CFPropertyList::List.new
137
+ plist.value = CFPropertyList.guess('autohide' => true, 'tilesize' => 48)
138
+ plist.save(path, CFPropertyList::List::FORMAT_XML)
139
+ _(MacOS::Defaults.new(domain: domain).to_h).must_equal('autohide' => true, 'tilesize' => 48)
140
+ ensure
141
+ File.delete(path) if path && File.exist?(path)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,48 @@
1
+ require_relative '../helper'
2
+ require 'MacOS/DiskUtil'
3
+
4
+ describe MacOS::DiskUtil do
5
+ def with_disks(&block)
6
+ MacOS::DiskUtil.stub(:xml_volume_plist, fixture('diskutil-list-plist.xml'), &block)
7
+ end
8
+
9
+ def with_df(&block)
10
+ MacOS::DiskUtil.stub(:`, fixture('df-k.txt'), &block)
11
+ end
12
+
13
+ describe "the volumes" do
14
+ it "keeps the disks with partitions" do
15
+ with_disks do
16
+ _(MacOS::DiskUtil.volumes_and_partitions.map{|disk| disk['DeviceIdentifier']}).must_equal(%w{disk0 disk4})
17
+ end
18
+ end
19
+
20
+ it "gives each partition's device, mount point, name and size" do
21
+ with_disks do
22
+ _(MacOS::DiskUtil.volumes.last).must_equal(device_id: 'disk4s1', mount_point: '/Volumes/EXAMPLE', name: 'EXAMPLE', size: 32006733824)
23
+ _(MacOS::DiskUtil.volumes.map{|volume| volume[:device_id]}).must_equal(%w{disk0s1 disk0s2 disk4s1})
24
+ end
25
+ end
26
+
27
+ it "gives those which are mounted as its external volumes" do
28
+ with_disks do
29
+ _(MacOS::DiskUtil.external_volumes.map{|volume| volume[:name]}).must_equal(%w{EXAMPLE})
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "the free space" do
35
+ it "gives the bytes available on a device or a volume" do
36
+ with_df do
37
+ _(MacOS::DiskUtil.free_bytes_for_device_id('disk4s1')).must_equal(31241024 * 1024)
38
+ _(MacOS::DiskUtil.free_bytes_for_volume_name('EXAMPLE')).must_equal(31241024 * 1024)
39
+ end
40
+ end
41
+
42
+ it "gives nil for a device which isn't there" do
43
+ with_df do
44
+ _(MacOS::DiskUtil.free_bytes_for_device_id('disk9s9')).must_be_nil
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,69 @@
1
+ require_relative '../helper'
2
+ require 'MacOS/Dmg'
3
+
4
+ describe MacOS::Dmg do
5
+ def run_quietly(&block)
6
+ Dir.stub(:chdir, nil){commands_run_by(MacOS::Dmg, &block)}
7
+ end
8
+
9
+ describe "the class" do
10
+ it "mounts, installs from and unmounts a disk image, escaping each name for the shell" do
11
+ commands = run_quietly do
12
+ MacOS::Dmg.mount('Example App.dmg')
13
+ MacOS::Dmg.run_installer(mount_point: 'Example App', pkg_name: 'Example App.pkg')
14
+ MacOS::Dmg.unmount('Example App')
15
+ end
16
+ _(commands).must_equal([
17
+ ['hdiutil attach Example\\ App.dmg'],
18
+ ["sudo installer -pkg Example\\ App.pkg -target '/'"],
19
+ ['hdiutil detach -Force /Volumes/Example\\ App'],
20
+ ])
21
+ end
22
+
23
+ it "installs from a disk image it's given the names of" do
24
+ commands = run_quietly{MacOS::Dmg.install(path_to_dmg: '/tmp/Example-1.0.dmg', disk_image_name: 'Example-1.0.dmg', package: 'Example')}
25
+ _(commands).must_equal([
26
+ ['hdiutil attach /tmp/Example-1.0.dmg'],
27
+ ["sudo installer -pkg Example.pkg -target '/'"],
28
+ ['hdiutil detach -Force /Volumes/Example-1'],
29
+ ])
30
+ end
31
+
32
+ it "prints each command where verbose" do
33
+ MacOS::Dmg.verbose = true
34
+ _{run_quietly{MacOS::Dmg.mount('Example.dmg')}}.must_output("hdiutil attach Example.dmg\n")
35
+ ensure
36
+ MacOS::Dmg.verbose = nil
37
+ end
38
+ end
39
+
40
+ describe "an instance" do
41
+ let(:dmg){MacOS::Dmg.new(path_to_dmg: '/tmp/Example-1.0.dmg', disk_image_name: 'Example-1.0.dmg', package: 'Example')}
42
+
43
+ it "takes its package's name and its mount point from its other names, where they aren't given" do
44
+ _(dmg.pkg_name).must_equal('Example.pkg')
45
+ _(dmg.mount_point).must_equal('Example-1')
46
+ end
47
+
48
+ it "takes them as given" do
49
+ dmg.pkg_name = 'Other.pkg'
50
+ dmg.mount_point = 'Other'
51
+ _(dmg.pkg_name).must_equal('Other.pkg')
52
+ _(dmg.mount_point).must_equal('Other')
53
+ end
54
+
55
+ it "answers to each of its disk image's names" do
56
+ dmg.filename = 'Renamed.dmg'
57
+ _([dmg.disk_image_name, dmg.filename, dmg.dmg_filename, dmg.image_name]).must_equal(['Renamed.dmg'] * 4)
58
+ end
59
+
60
+ it "mounts, installs and unmounts" do
61
+ commands = run_quietly{dmg.install}
62
+ _(commands).must_equal([
63
+ ['hdiutil attach /tmp/Example-1.0.dmg'],
64
+ ["sudo installer -pkg Example.pkg -target '/'"],
65
+ ['hdiutil detach -Force /Volumes/Example-1'],
66
+ ])
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,46 @@
1
+ require_relative '../helper'
2
+ require 'MacOS/HardwarePort'
3
+
4
+ describe MacOS::HardwarePort do
5
+ def with_ports(&block)
6
+ MacOS::HardwarePort.stub(:output, fixture('networksetup-listallhardwareports.txt'), &block)
7
+ end
8
+
9
+ describe ".parse" do
10
+ it "makes one for each hardware port networksetup lists" do
11
+ ports = MacOS::HardwarePort.parse(fixture('networksetup-listallhardwareports.txt'))
12
+ _(ports.map(&:interface)).must_equal(%w{en4 en5 en6 en8 bridge0 en0 en1 en2 en3})
13
+ end
14
+
15
+ it "reads each port's name, device and Ethernet address" do
16
+ wifi = MacOS::HardwarePort.parse(fixture('networksetup-listallhardwareports.txt')).find{|port| port.device == 'en0'}
17
+ _(wifi.hardware_port).must_equal('Wi-Fi')
18
+ _(wifi.name).must_equal('Wi-Fi')
19
+ _(wifi.ethernet_address).must_equal('00:00:5e:00:53:08')
20
+ _(wifi.mac_address).must_equal('00:00:5e:00:53:08')
21
+ end
22
+ end
23
+
24
+ describe "finding" do
25
+ it "finds a port by its interface, its name or its MAC address" do
26
+ with_ports do
27
+ _(MacOS::HardwarePort.find_by_interface('en0').name).must_equal('Wi-Fi')
28
+ _(MacOS::HardwarePort.find_by_device('bridge0').name).must_equal('Thunderbolt Bridge')
29
+ _(MacOS::HardwarePort.find_by_name('Wi-Fi').interface).must_equal('en0')
30
+ _(MacOS::HardwarePort.find_by_hardware_port('Thunderbolt 2').interface).must_equal('en2')
31
+ _(MacOS::HardwarePort.find_by_mac_address('00:00:5e:00:53:04').interface).must_equal('en4')
32
+ _(MacOS::HardwarePort.find_by_ethernet_address('00:00:5e:00:53:04').interface).must_equal('en4')
33
+ end
34
+ end
35
+
36
+ it "finds nothing for a port which isn't there" do
37
+ with_ports do
38
+ _(MacOS::HardwarePort.find_by_name('Nowhere')).must_be_nil
39
+ end
40
+ end
41
+ end
42
+
43
+ it "keeps the raw output to itself" do
44
+ _(MacOS::HardwarePort.respond_to?(:output)).must_equal(false)
45
+ end
46
+ end
@@ -0,0 +1,144 @@
1
+ require_relative '../helper'
2
+ require 'MacOS/IfConfig'
3
+
4
+ describe MacOS::IfConfig do
5
+ let(:output){fixture('ifconfig-a.txt')}
6
+
7
+ def with_ifconfig(output = fixture('ifconfig-a.txt'), &block)
8
+ MacOS::IfConfig.stub(:output, output, &block)
9
+ end
10
+
11
+ def interface(name)
12
+ MacOS::IfConfig.parse(output).find{|ifconfig| ifconfig.interface == name}
13
+ end
14
+
15
+ describe ".parse" do
16
+ it "makes one for each interface, in the order ifconfig lists them" do
17
+ _(MacOS::IfConfig.parse(output).map(&:interface)).must_equal(%w{lo0 gif0 stf0 en1 en0 awdl0 bridge0 utun0 utun4})
18
+ end
19
+
20
+ it "reads an interface's address, netmask and broadcast address" do
21
+ en0 = interface('en0')
22
+ _(en0.ipv4_address).must_equal('192.0.2.114')
23
+ _(en0.netmask).must_equal('0xffffff00')
24
+ _(en0.broadcast_address).must_equal('192.0.2.255')
25
+ end
26
+
27
+ it "reads an address without a broadcast address" do
28
+ _(interface('lo0').ipv4_address).must_equal('127.0.0.1')
29
+ _(interface('lo0').netmask).must_equal('0xff000000')
30
+ end
31
+
32
+ it "reads the near end of a point-to-point address" do
33
+ _(interface('utun4').ipv4_address).must_equal('198.51.100.250')
34
+ _(interface('utun4').netmask).must_equal('0xffffffff')
35
+ end
36
+
37
+ it "reads the MAC address, status and flags" do
38
+ en0 = interface('en0')
39
+ _(en0.mac_address).must_equal('00:00:5e:00:53:02')
40
+ _(en0.status).must_equal('active')
41
+ _(en0.flags).must_include('UP')
42
+ end
43
+
44
+ it "reads lines with a trailing space, as older ifconfigs give them" do
45
+ older = "en0: flags=8863<UP,BROADCAST> mtu 1500\n\tether 00:00:5e:00:53:02 \n\tinet 192.0.2.114 netmask 0xffffff00 \n"
46
+ en0 = MacOS::IfConfig.parse(older).first
47
+ _(en0.mac_address).must_equal('00:00:5e:00:53:02')
48
+ _(en0.ipv4_address).must_equal('192.0.2.114')
49
+ _(en0.netmask).must_equal('0xffffff00')
50
+ end
51
+
52
+ it "gives the same each time" do
53
+ first = MacOS::IfConfig.parse(output).map(&:interface)
54
+ _(MacOS::IfConfig.parse(output).map(&:interface)).must_equal(first)
55
+ end
56
+
57
+ it "gives none for output with no interfaces" do
58
+ _(MacOS::IfConfig.parse('')).must_equal([])
59
+ end
60
+ end
61
+
62
+ describe "the interfaces in each state" do
63
+ it "lists those which are active and inactive" do
64
+ with_ifconfig do
65
+ _(MacOS::IfConfig.active_interfaces_names).must_equal(%w{en0 awdl0})
66
+ _(MacOS::IfConfig.inactive_interfaces_names).must_equal(%w{en1 bridge0})
67
+ end
68
+ end
69
+
70
+ it "lists those which are up: active or flagged UP, and with an address" do
71
+ with_ifconfig do
72
+ _(MacOS::IfConfig.up_interfaces_names).must_equal(%w{lo0 en0 utun4})
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "the interfaces themselves" do
78
+ it "lists every interface's name, and those active and inactive" do
79
+ with_ifconfig do
80
+ _(MacOS::IfConfig.interface_names).must_equal(%w{lo0 gif0 stf0 en1 en0 awdl0 bridge0 utun0 utun4})
81
+ _(MacOS::IfConfig.active_interfaces.map(&:name)).must_equal(%w{en0 awdl0})
82
+ _(MacOS::IfConfig.inactive_interfaces.map(&:name)).must_equal(%w{en1 bridge0})
83
+ end
84
+ end
85
+ end
86
+
87
+ describe "the addresses" do
88
+ it "lists every MAC address and IPv4 address" do
89
+ with_ifconfig do
90
+ _(MacOS::IfConfig.mac_addresses).must_equal(%w{00:00:5e:00:53:01 00:00:5e:00:53:02 00:00:5e:00:53:03 00:00:5e:00:53:01})
91
+ _(MacOS::IfConfig.ipv4_addresses).must_equal(%w{127.0.0.1 192.0.2.114 198.51.100.250})
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "finding" do
97
+ it "finds an interface by its name, MAC address or IPv4 address" do
98
+ with_ifconfig do
99
+ _(MacOS::IfConfig.find_by_interface('en0').mac_address).must_equal('00:00:5e:00:53:02')
100
+ _(MacOS::IfConfig.find_by_mac_address('00:00:5e:00:53:03').interface).must_equal('awdl0')
101
+ _(MacOS::IfConfig.find_by_ipv4_address('198.51.100.250').interface).must_equal('utun4')
102
+ end
103
+ end
104
+
105
+ it "matches names against a Regexp, or a String taken literally" do
106
+ with_ifconfig do
107
+ _(MacOS::IfConfig.all_matching(/\Autun\d/).map(&:name)).must_equal(%w{utun0 utun4})
108
+ _(MacOS::IfConfig.all_matching('en').map(&:name)).must_equal(%w{en1 en0})
109
+ _(MacOS::IfConfig.all_matching('.')).must_equal([])
110
+ end
111
+ end
112
+ end
113
+
114
+ describe "bringing interfaces down" do
115
+ it "brings down only those matching which are up" do
116
+ with_ifconfig do
117
+ commands = commands_run_by(MacOS::IfConfig) do
118
+ _(MacOS::IfConfig.bring_down_matching_interfaces(/\Autun/)).must_equal(%w{utun4})
119
+ end
120
+ _(commands).must_equal([%w{sudo ifconfig utun4 down}])
121
+ end
122
+ end
123
+
124
+ it "destroys an interface" do
125
+ commands = commands_run_by(MacOS::IfConfig){MacOS::IfConfig.destroy_interface('utun4')}
126
+ _(commands).must_equal([%w{sudo ifconfig utun4 destroy}])
127
+ end
128
+ end
129
+
130
+ describe "an interface" do
131
+ it "gives its MAC address as its Ethernet address too" do
132
+ _(interface('en0').ethernet_address).must_equal('00:00:5e:00:53:02')
133
+ end
134
+
135
+ it "knows whether it's virtual" do
136
+ _(interface('utun4').virtual?).wont_be_nil
137
+ _(interface('en0').virtual?).must_be_nil
138
+ end
139
+ end
140
+
141
+ it "keeps the raw output to itself" do
142
+ _(MacOS::IfConfig.respond_to?(:output)).must_equal(false)
143
+ end
144
+ end
@@ -0,0 +1,22 @@
1
+ require_relative '../helper'
2
+ require 'MacOS/Keychain'
3
+
4
+ describe MacOS::Keychain do
5
+ it "asks security for a generic password, the key escaped for the shell" do
6
+ _(MacOS::Keychain.find_generic_password_command('Example Net')).must_equal('security find-generic-password -ga Example\\ Net')
7
+ end
8
+
9
+ it "gives a Wi-Fi network's password, which security writes to standard error" do
10
+ found = ["keychain: \"/Library/Keychains/System.keychain\"\n", "password: \"example-passphrase\"\n", nil]
11
+ MacOS::Keychain.stub(:find_generic_password, found) do
12
+ _(MacOS::Keychain.wifi_password('ExampleNet')).must_equal('example-passphrase')
13
+ end
14
+ end
15
+
16
+ it "gives nil where the keychain has no password for the network" do
17
+ missing = ['', "security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.\n", nil]
18
+ MacOS::Keychain.stub(:find_generic_password, missing) do
19
+ _(MacOS::Keychain.wifi_password('Nowhere')).must_be_nil
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../helper'
2
+ require 'MacOS/LaunchControl'
3
+
4
+ describe MacOS::LaunchControl do
5
+ let(:plist){'/Library/LaunchDaemons/com.example.agent.plist'}
6
+
7
+ it "loads and unloads a plist" do
8
+ commands = commands_run_by(MacOS::LaunchControl) do
9
+ MacOS::LaunchControl.load(plist)
10
+ MacOS::LaunchControl.unload(plist)
11
+ end
12
+ _(commands).must_equal([["launchctl load -w #{plist}"], ["launchctl unload -w #{plist}"]])
13
+ end
14
+
15
+ it "escapes the plist's path for the shell" do
16
+ commands = commands_run_by(MacOS::LaunchControl){MacOS::LaunchControl.load('/Library/Launch Agents/com.example.agent.plist')}
17
+ _(commands).must_equal([['launchctl load -w /Library/Launch\ Agents/com.example.agent.plist']])
18
+ end
19
+
20
+ describe "an instance" do
21
+ it "holds the plist it's made with" do
22
+ _(MacOS::LaunchControl.new(plist).path_to_plist).must_equal(plist)
23
+ _(MacOS::LaunchControl.new.path_to_plist).must_be_nil
24
+ end
25
+
26
+ it "loads and unloads its own plist" do
27
+ launch_control = MacOS::LaunchControl.new(plist)
28
+ commands = commands_run_by(MacOS::LaunchControl) do
29
+ launch_control.load
30
+ launch_control.unload
31
+ end
32
+ _(commands).must_equal([["launchctl load -w #{plist}"], ["launchctl unload -w #{plist}"]])
33
+ end
34
+
35
+ it "unloads another plist where it's given one" do
36
+ commands = commands_run_by(MacOS::LaunchControl){MacOS::LaunchControl.new.unload('/tmp/other.plist')}
37
+ _(commands).must_equal([['launchctl unload -w /tmp/other.plist']])
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,69 @@
1
+ require_relative '../helper'
2
+ require 'MacOS/SystemProfiler'
3
+
4
+ describe MacOS::SystemProfiler do
5
+ def with_output(output = fixture('system_profiler-SPSoftwareDataType.txt'), &block)
6
+ MacOS::SystemProfiler.stub(:system_profiler_output, output, &block)
7
+ end
8
+
9
+ describe "the system's version" do
10
+ it "reads it from the System Version field" do
11
+ with_output do
12
+ _(MacOS::SystemProfiler.system_version_data).must_equal('macOS 15.8.1 (24H32)')
13
+ _(MacOS::SystemProfiler.system_version_string).must_equal('15.8.1')
14
+ end
15
+ end
16
+
17
+ it "reads the version of an older OS X" do
18
+ with_output(" System Version: Mac OS X 10.9.5 (13F34)\n") do
19
+ _(MacOS::SystemProfiler.system_version_string).must_equal('10.9.5')
20
+ end
21
+ end
22
+
23
+ it "gives it as a Version, and its major, minor and tiny parts" do
24
+ with_output do
25
+ _(MacOS::SystemProfiler.system_version).must_be_instance_of(Version)
26
+ _(MacOS::SystemProfiler.system_version.to_s).must_equal('15.8.1')
27
+ _(MacOS::SystemProfiler.major_system_version).must_equal('15')
28
+ _(MacOS::SystemProfiler.minor_system_version).must_equal('8')
29
+ _(MacOS::SystemProfiler.tiny_system_version).must_equal('1')
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "the kernel" do
35
+ it "reads its name and version" do
36
+ with_output do
37
+ _(MacOS::SystemProfiler.kernel_version_data).must_equal('Darwin 24.6.0')
38
+ _(MacOS::SystemProfiler.kernel_name).must_equal('Darwin')
39
+ _(MacOS::SystemProfiler.kernel_version_string).must_equal('24.6.0')
40
+ _(MacOS::SystemProfiler.kernel_version.to_s).must_equal('24.6.0')
41
+ end
42
+ end
43
+ end
44
+
45
+ describe "the other fields" do
46
+ it "reads each, by its method and by its aliases" do
47
+ with_output do
48
+ sp = MacOS::SystemProfiler
49
+ _([sp.boot_volume_data, sp.boot_volume_string]).must_equal(['Macintosh HD'] * 2)
50
+ _([sp.boot_mode_data, sp.boot_mode_string, sp.boot_mode]).must_equal(['Normal'] * 3)
51
+ _([sp.computer_name_data, sp.computer_name_string, sp.computer_name]).must_equal(['Example Mac'] * 3)
52
+ _([sp.username_data, sp.username_string, sp.username]).must_equal(['Example User (example)'] * 3)
53
+ _([sp.secure_virtual_memory_data, sp.secure_virtual_memory_string, sp.secure_virtual_memory]).must_equal(['Enabled'] * 3)
54
+ _([sp.system_integrity_protection_data, sp.system_integrity_protection_string, sp.system_integrity_protection]).must_equal(['Enabled'] * 3)
55
+ _([sp.time_since_boot_data, sp.time_since_boot_string, sp.time_since_boot]).must_equal(['6 days, 2 hours, 34 minutes'] * 3)
56
+ end
57
+ end
58
+
59
+ it "gives nil for a field which isn't there" do
60
+ with_output('') do
61
+ _(MacOS::SystemProfiler.boot_mode).must_be_nil
62
+ end
63
+ end
64
+ end
65
+
66
+ it "keeps its reading of a field to itself" do
67
+ _(MacOS::SystemProfiler.respond_to?(:field)).must_equal(false)
68
+ end
69
+ end
@@ -0,0 +1,20 @@
1
+ require_relative '../helper'
2
+ require 'MacOS/USBFlashDrive'
3
+
4
+ describe MacOS::USBFlashDrive do
5
+ it "asks system_profiler for the USB devices" do
6
+ _(MacOS::USBFlashDrive.system_profiler_usb_data_type_command).must_equal('system_profiler SPUSBDataType')
7
+ end
8
+
9
+ it "gives every flash drive's mount point" do
10
+ MacOS::USBFlashDrive.stub(:raw_system_profiler_output, [fixture('system_profiler-SPUSBDataType.txt'), '', nil]) do
11
+ _(MacOS::USBFlashDrive.all_mount_points).must_equal(['/Volumes/EXAMPLE', '/Volumes/Example 2'])
12
+ end
13
+ end
14
+
15
+ it "gives none where no flash drive is mounted" do
16
+ MacOS::USBFlashDrive.stub(:raw_system_profiler_output, ["USB:\n", '', nil]) do
17
+ _(MacOS::USBFlashDrive.all_mount_points).must_equal([])
18
+ end
19
+ end
20
+ end