mitchellh-rubyuw 0.7.2 → 0.99.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.
data/lib/rubyuw/sln.rb ADDED
@@ -0,0 +1,108 @@
1
+ module RubyUW
2
+ # RubyUW::SLN is used to find and extract information about
3
+ # specific SLNs. It grabs SLN information via the MyUW time
4
+ # schedule.
5
+ #
6
+ # <b>Requires authentication with RubyUW::Base prior to use!</b>
7
+ #
8
+ # Additionally, this class should typically never be instantiated
9
+ # by any developer. Instead, the class methods should be used
10
+ # which will return an instance of the SLN object.
11
+ #
12
+ # == Usage Examples
13
+ #
14
+ # require 'rubyuw'
15
+ # RubyUW::Base.authenticate("netid", "password")
16
+ # sln_info = RubyUW::SLN.find("12345", "AUT+2009")
17
+ class SLN
18
+ attr_reader :sln
19
+ attr_reader :data
20
+
21
+ # Initialize a new SLN object. This should never be called on its
22
+ # own, but instead you should use the find method to setup a
23
+ # sln.
24
+ #
25
+ # @param [String] SLN number in string format.
26
+ # @param [String] Term that the SLN corresponds to.
27
+ # @param [Hash] Data of key to value.
28
+ def initialize(sln, term, data)
29
+ @sln = sln
30
+ @term = term
31
+ @data = data
32
+ end
33
+
34
+ # Grab the data of the SLN based on a key. Returns the value of
35
+ # a field for the SLN, or nil otherwise. Supported fields coming
36
+ # soon.
37
+ def data(key)
38
+ @data[key.to_sym]
39
+ end
40
+
41
+ class <<self
42
+ # Finds information about a specific SLN. The SLN information
43
+ # is grabbed from the MyUW time schedule page. Authentication is
44
+ # required prior to use.
45
+ #
46
+ # @param [#to_s] The SLN, which must respond to #to_s
47
+ # @param [String] The term, as represented in the time schedule URL.
48
+ # @return [RubyUW::SLN]
49
+ def find(sln_number, term)
50
+ raise Errors::NotLoggedInError.new unless Base.authenticated?
51
+
52
+ # Grab the SLN page and check for various failure scenarios
53
+ page = Base.connection.get("https://sdb.admin.washington.edu/timeschd/uwnetid/sln.asp?QTRYR=#{term}&SLN=#{sln_number}")
54
+ raise Errors::SLNRequestedTooSoonError.new if requested_too_soon?(page)
55
+ raise Errors::SLNDoesNotExistError.new if !sln_exists?(page)
56
+ raise Errors::SLNServiceClosedError.new if time_schedule_closed?(page)
57
+
58
+ # Now that the page is probably valid, we extract the field data
59
+ # based on various xpaths.
60
+ new(sln_number, term, extract_data_from_page(page))
61
+ end
62
+
63
+ protected
64
+
65
+ def requested_too_soon?(page)
66
+ page.uri.to_s == "http://www.washington.edu/students/timeschd/badrequest.html"
67
+ end
68
+
69
+ def sln_exists?(page)
70
+ page.body.to_s.match(/SLN: (.{5,6}) does not exist./).nil?
71
+ end
72
+
73
+ def time_schedule_closed?(page)
74
+ page.uri.to_s == "http://www.washington.edu/students/timeschd/nots.html"
75
+ end
76
+
77
+ def extract_data_from_page(page)
78
+ extract_data = [
79
+ [[nil, :course, :section, :type, :credits, :title], "//table[@border=1 and @cellpadding=3]//tr[@rowspan=1]//td"],
80
+ [[:current_enrollment, :limit_enrollment, :room_capacity, :space_available, :status], "//table[@border=1 and @cellpadding=3]//tr[count(td)=5]//td"],
81
+ [[:notes], "//table[@border=1 and @cellpadding=3]//tr[count(td)=1]//preceding-sibling::tr[count(th)=1]//following-sibling::tr//td"]
82
+ ]
83
+
84
+ data = {}
85
+ extract_data.each do |keys, xpath|
86
+ data.merge!(Base.extract(page, xpath, keys))
87
+ end
88
+
89
+ data
90
+ end
91
+ end
92
+ end
93
+
94
+ module Errors
95
+ # An error indicating that an SLN was requested back-to-back too
96
+ # quickly. MyUW enforces a timeout between requests for SLN information,
97
+ # and when this is not obeyed, this error will be thrown by RubyUW.
98
+ class SLNRequestedTooSoonError < Exception; end
99
+
100
+ # An error indicating that an SLN requested is invalid (does not
101
+ # exist).
102
+ class SLNDoesNotExistError < Exception; end
103
+
104
+ # An error indicating that the time scheduling service of MyUW
105
+ # is currently closed.
106
+ class SLNServiceClosedError < Exception; end
107
+ end
108
+ end
@@ -0,0 +1,47 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class LiveCurriculumEnrollmentTest < Test::Unit::TestCase
4
+ context "live curriculum info grabbing" do
5
+ setup do
6
+ @curriculum = "CHEM"
7
+ @term = "AUT+2009"
8
+
9
+ @curriculum_info = {
10
+ "11633" => {
11
+ :course => "CHEM 110",
12
+ :section => "A",
13
+ :type => "LC"
14
+ },
15
+ "11635" => {
16
+ :course => "CHEM 110",
17
+ :section => "AB",
18
+ :type => "QZ"
19
+ },
20
+ "11826" => {
21
+ :course => "CHEM 475",
22
+ :section => "A",
23
+ :type => "LC"
24
+ }
25
+ }
26
+ end
27
+
28
+ should "raise an error for an invalid SLN/quarter" do
29
+ authenticate
30
+ assert_raise(RubyUW::Errors::CurriculumDoesNotExistError) { RubyUW::CurriculumEnrollment.find("NOTCHEM", @term) }
31
+ end
32
+
33
+ should "grab all SLNs properly" do
34
+ authenticate
35
+ results = RubyUW::CurriculumEnrollment.find(@curriculum, @term)
36
+
37
+ @curriculum_info.each do |sln, info|
38
+ course = results[sln]
39
+ assert !course.nil?
40
+
41
+ info.each do |k,v|
42
+ assert_equal v, course.data(k)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -3,13 +3,6 @@ require File.join(File.dirname(__FILE__), 'test_helper')
3
3
  class LiveSLNTest < Test::Unit::TestCase
4
4
  context "live SLN info grabbing" do
5
5
  setup do
6
- @myuw = MyUW.new
7
- @session = @myuw.session
8
- @session.email = MYUW_ID
9
- @session.password = MYUW_PASSWORD
10
- @session.login
11
- assert @session.logged_in? # Sanity
12
-
13
6
  @sln_info = {
14
7
  :sln => "17237",
15
8
  :quarter => "AUT+2009",
@@ -26,39 +19,36 @@ class LiveSLNTest < Test::Unit::TestCase
26
19
  :notes => "Quiz Section"
27
20
  }
28
21
  }
29
-
30
- @sln = @myuw.sln(@sln_info[:sln], @sln_info[:quarter])
31
- assert @sln # Sanity
32
22
  end
33
23
 
34
24
  teardown do
35
- sleep(5) # To avoid the timeout problems within tests
25
+ sleep(5)
36
26
  end
37
27
 
38
- should "grab course info for a valid SLN and quarter" do
28
+ def get_sln_obj
29
+ RubyUW::SLN.find(@sln_info[:sln], @sln_info[:quarter])
30
+ end
31
+
32
+ should "grab course info for a valid SLN" do
33
+ authenticate
34
+ @sln = get_sln_obj
35
+
39
36
  @sln_info[:info].each do |k,v|
40
- assert_equal v, @sln.send(k), "#{k} should've been #{v}"
37
+ assert_equal v, @sln.data(k), "#{k} should've been #{v}"
41
38
  end
42
39
  end
43
40
 
44
41
  should "raise an error if requests are back to back too fast" do
45
- assert_nothing_raised do
46
- @sln.course
47
- end
48
-
49
- @sln.sln = @sln.sln # Force reload
50
-
51
- assert_raise MyUW::RequestSLNTooSoonError do
52
- @sln.course
53
- end
42
+ authenticate
43
+ assert_nothing_raised() { get_sln_obj }
44
+ assert_raise(RubyUW::Errors::SLNRequestedTooSoonError) { get_sln_obj }
54
45
  end
55
46
 
56
47
  should "raise an error for an invalid SLN/quarter" do
57
- @sln.sln = 91919 # Tested to not exist
48
+ @sln_info[:sln] = 91919 # Tested to not exist
58
49
 
59
- assert_raise MyUW::InvalidSLNError do
60
- @sln.course
61
- end
50
+ authenticate
51
+ assert_raise(RubyUW::Errors::SLNDoesNotExistError) { get_sln_obj }
62
52
  end
63
53
  end
64
54
  end
@@ -4,8 +4,21 @@ rescue LoadError => e
4
4
  abort("Running live tests requires a password.rb file in the test folder with your MyUW credentials.")
5
5
  end
6
6
 
7
- require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'myuw')
7
+ require 'rubygems'
8
+ $:.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
9
+ require 'rubyuw'
8
10
  require 'test/unit'
9
11
  require "contest"
10
12
  require "stories"
11
- require "mocha"
13
+ require "mocha"
14
+
15
+ class Test::Unit::TestCase
16
+ def setup
17
+ RubyUW::Base.reset_connection!
18
+ end
19
+
20
+ def authenticate
21
+ assert RubyUW::Base.authenticate(MYUW_ID, MYUW_PASSWORD)
22
+ assert RubyUW::Base.authenticated? # sanity
23
+ end
24
+ end
@@ -0,0 +1,171 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class BaseTest < Test::Unit::TestCase
4
+ context "with connection" do
5
+ setup do
6
+ RubyUW::Base.reset_connection!
7
+ end
8
+
9
+ should "initialize a WWW::Mechanize object on the first call" do
10
+ RubyUW::Connection.expects(:new).once
11
+ RubyUW::Base.connection
12
+ end
13
+
14
+ should "not initialize WWW::Mechanize objects on subsequent calls" do
15
+ RubyUW::Connection.expects(:new).once.returns(true)
16
+ 50.times { RubyUW::Base.connection }
17
+ end
18
+
19
+ should "clear out the connection to force recreation of object on reset_connection!" do
20
+ assert !RubyUW::Base.connection.nil?
21
+ RubyUW::Base.reset_connection!
22
+ RubyUW::Connection.expects(:new).once.returns(true)
23
+ 50.times { RubyUW::Base.connection }
24
+ end
25
+ end
26
+
27
+ context "with authentication" do
28
+ setup do
29
+ # initialize the base connection
30
+ @conn = RubyUW::Base.reset_connection!.connection
31
+ @conn.follow_meta_refresh = false
32
+
33
+ @good_id = "foo"
34
+ @good_password = "bar"
35
+ @bad_password = @good_password + "wrong"
36
+ end
37
+
38
+ should "clear the cookie jar of the connection" do
39
+ RubyUW::Base.expects(:logout).once
40
+
41
+ result_page = mock('page')
42
+ result_page.stubs(:form_with).returns(true)
43
+ @conn.stubs(:execute!).returns(result_page)
44
+
45
+ RubyUW::Base.authenticate(@good_id, @good_password)
46
+ end
47
+
48
+ should "succeed login with good ID and password and valid pages" do
49
+ mock_flow([
50
+ [:goto, ["http://myuw.washington.edu"], "welcome"],
51
+ [:submit_form, ["f", anything], "not_logged_in_relay"],
52
+ [:submit_form, ["relay", anything], "login"],
53
+ [:submit_form, ["query", anything], "not_logged_in_relay"]
54
+ ])
55
+
56
+ assert RubyUW::Base.authenticate(@good_id, @good_password)
57
+ end
58
+
59
+ should "fail login with bad ID and password" do
60
+ mock_flow([
61
+ [:goto, ["http://myuw.washington.edu"], "welcome"],
62
+ [:submit_form, ["f", anything], "not_logged_in_relay"],
63
+ [:submit_form, ["relay", anything], "login"],
64
+ [:submit_form, ["query", anything], "login"]
65
+ ])
66
+
67
+ assert !RubyUW::Base.authenticate(@good_id, @bad_password)
68
+ end
69
+ end
70
+
71
+ context "with authentication check" do
72
+ should "return true if it can find the correct xpath" do
73
+ mock_flow([
74
+ [:goto, ["http://myuw.washington.edu"], "welcome"],
75
+ [:submit_form, ["f", anything], "logged_in_relay"]
76
+ ])
77
+
78
+ assert RubyUW::Base.authenticated?
79
+ end
80
+
81
+ should "not return true if it can't find the greeting" do
82
+ mock_flow([
83
+ [:goto, ["http://myuw.washington.edu"], "welcome"],
84
+ [:submit_form, ["f", anything], "login"]
85
+ ])
86
+
87
+ assert !RubyUW::Base.authenticated?
88
+ end
89
+ end
90
+
91
+ context "with logging out" do
92
+ should "clear the cookie jar on logout" do
93
+ cookie_jar = mock('cookie_jar')
94
+ cookie_jar.expects(:clear!).once
95
+
96
+ conn = RubyUW::Base.connection
97
+ conn.expects(:cookie_jar).returns(cookie_jar)
98
+
99
+ RubyUW::Base.logout
100
+ end
101
+ end
102
+
103
+ context "with extracting data from page" do
104
+ setup do
105
+ @page = mock('page')
106
+ @xpath = "foo"
107
+ end
108
+
109
+ def setup_nodes(inner_texts)
110
+ nodes = []
111
+ inner_texts.each do |text|
112
+ node = mock('node')
113
+ node.stubs(:inner_text).returns(text)
114
+
115
+ nodes << node
116
+ end
117
+
118
+ nodes
119
+ end
120
+
121
+ should "run the xpath on the specified page" do
122
+ @page.expects(:search).with(@xpath).returns([])
123
+ RubyUW::Base.extract(@page, @xpath, [])
124
+ end
125
+
126
+ should "return nil if the xpath returns an empty array" do
127
+ @page.expects(:search).with(@xpath).returns([])
128
+ assert_nil RubyUW::Base.extract(@page, @xpath, [])
129
+ end
130
+
131
+ should "map keys to node results if xpath returns valid nodes" do
132
+ expected = { :one => "1", :two => "2", :three => "3" }
133
+ nodes = setup_nodes(expected.values)
134
+ keys = expected.keys
135
+
136
+ @page.expects(:search).with(@xpath).returns(nodes)
137
+ result = RubyUW::Base.extract(@page, @xpath, keys)
138
+ assert expected == result
139
+ end
140
+
141
+ should "skip the nil keys (while continuing to go through the node list)" do
142
+ nodes = setup_nodes(["1","2","3"])
143
+ keys = [:one, nil, :three]
144
+
145
+ @page.expects(:search).with(@xpath).returns(nodes)
146
+ result = RubyUW::Base.extract(@page, @xpath, keys)
147
+ assert_equal 2, result.length
148
+ assert !result.value?("2")
149
+ end
150
+
151
+ should "return what it has so far if it runs out of keys" do
152
+ nodes = setup_nodes((1..50).to_a.map(&:to_s))
153
+ keys = [:one]
154
+
155
+ @page.expects(:search).with(@xpath).returns(nodes)
156
+ result = RubyUW::Base.extract(@page, @xpath, keys)
157
+ assert_equal keys.length, result.length
158
+ assert_equal "1", result[keys[0]]
159
+ end
160
+
161
+ should "get rid of odd characters that MyUW has in text" do
162
+ nodes = setup_nodes(["foo\302\240"])
163
+ keys = [:one]
164
+
165
+ @page.expects(:search).with(@xpath).returns(nodes)
166
+ result = RubyUW::Base.extract(@page, @xpath, keys)
167
+ assert_equal keys.length, result.length
168
+ assert_equal "foo", result[keys[0]]
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,178 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class ConnectionTest < Test::Unit::TestCase
4
+ should "be a kind of WWW::Mechanize" do
5
+ assert RubyUW::Connection.new.kind_of?(WWW::Mechanize)
6
+ end
7
+
8
+ context "web flows" do
9
+ setup do
10
+ @conn = RubyUW::Connection.new
11
+ end
12
+
13
+ should "be empty to begin with" do
14
+ assert @conn.current_flow.empty?
15
+ end
16
+
17
+ context "with goto" do
18
+ should "add new item to flow and return self when calling goto" do
19
+ url = "http://foo.com"
20
+ assert_equal @conn, @conn.goto(url)
21
+ assert_equal 1, @conn.current_flow.length
22
+ assert_equal :goto, @conn.current_flow[0][0]
23
+ end
24
+
25
+ should "get the page when executing" do
26
+ @url = "http://foo.com"
27
+ @conn.goto(@url)
28
+ @conn.expects(:get).with(@url).once
29
+ @conn.execute!
30
+ end
31
+ end
32
+
33
+ context "with verify" do
34
+ setup do
35
+ @xpath = "//foo"
36
+ @url = "fake"
37
+ end
38
+
39
+ def mock_context(return_value=nil)
40
+ context = mock('context')
41
+ context.expects(:search).with(@xpath).once.returns(return_value)
42
+ @conn.expects(:execute_goto!).returns(context)
43
+ @conn.goto(@url)
44
+
45
+ context
46
+ end
47
+
48
+ should "add new item to flow and return self when calling verify" do
49
+ xpath = "//foo"
50
+ assert_equal @conn, @conn.verify(xpath)
51
+ assert_equal 1, @conn.current_flow.length
52
+ assert_equal :verify, @conn.current_flow[0][0]
53
+ end
54
+
55
+ should "verify the previous page result when executing" do
56
+ mock_context([1])
57
+ @conn.verify(@xpath)
58
+
59
+ assert_nothing_raised { @conn.execute! }
60
+ end
61
+
62
+ should "return back the context given" do
63
+ context = mock_context([1])
64
+ @conn.verify(@xpath)
65
+
66
+ assert_equal context, @conn.execute!
67
+ end
68
+
69
+ should "raise an InvalidPageError if no elements found" do
70
+ mock_context([])
71
+ @conn.verify(@xpath)
72
+
73
+ assert_raises(RubyUW::Errors::InvalidPageError) { @conn.execute! }
74
+ end
75
+ end
76
+
77
+ context "with submitting form" do
78
+ setup do
79
+ @url = "fake"
80
+ @form = "form"
81
+ @params = { :key => :value, :key2 => :value2 }
82
+ end
83
+
84
+ def mock_context
85
+ context = mock('context')
86
+ @conn.expects(:execute_goto!).returns(context)
87
+ @conn.goto(@url)
88
+
89
+ context
90
+ end
91
+
92
+ def mock_form_object(context)
93
+ form_obj = mock('form_obj')
94
+ form_obj.stubs(:[]=)
95
+ form_obj.expects(:submit).returns(:submit)
96
+ context.expects(:form_with).with(:name => @form).once.returns(form_obj)
97
+ form_obj
98
+ end
99
+
100
+ should "add new item to flow and return self when calling submit_form" do
101
+ assert_equal @conn, @conn.submit_form(@form, @params)
102
+ assert_equal 1, @conn.current_flow.length
103
+ assert_equal :submit_form, @conn.current_flow[0][0]
104
+ end
105
+
106
+ should "raise InvalidPageError if the form is not found on the page" do
107
+ context = mock_context
108
+ context.expects(:form_with).with(:name => @form).once.returns(nil)
109
+ @conn.submit_form(@form, @params)
110
+
111
+ assert_raises(RubyUW::Errors::InvalidPageError) { @conn.execute! }
112
+ end
113
+
114
+ should "assign the parameters to the form object" do
115
+ context = mock_context
116
+ form_obj = mock_form_object(context)
117
+ form_obj.expects(:[]=).times(@params.length)
118
+
119
+ @conn.submit_form(@form, @params)
120
+ @conn.execute!
121
+ end
122
+
123
+ should "send the parameters after #to_s'ing them" do
124
+ key = mock('para_key')
125
+ key.expects(:to_s).once
126
+
127
+ param = mock('param')
128
+ param.expects(:to_s).once
129
+
130
+ params = { key => param }
131
+
132
+ context = mock_context
133
+ form_obj = mock_form_object(context)
134
+ form_obj.expects(:[]=).times(params.length)
135
+
136
+ @conn.submit_form(@form, params)
137
+ @conn.execute!
138
+ end
139
+
140
+ should "submit and return the form result" do
141
+ context = mock_context
142
+ mock_form_object(context)
143
+ @conn.submit_form(@form, @params)
144
+
145
+ assert_equal :submit, @conn.execute!
146
+ end
147
+ end
148
+
149
+ context "with executing" do
150
+ setup do
151
+ @url = "foo"
152
+ @form_name = "form"
153
+ @form_params = {}
154
+
155
+ @conn.goto(@url).submit_form(@form_name, @form_params)
156
+ @conn.stubs(:execute_goto!).returns(:goto)
157
+ @conn.stubs(:execute_submit_form!).returns(:submit_form)
158
+ end
159
+
160
+ should "execute in the order given" do
161
+ execution_sequence = sequence('execution')
162
+ @conn.expects(:execute_goto!).in_sequence(execution_sequence)
163
+ @conn.expects(:execute_submit_form!).in_sequence(execution_sequence)
164
+ @conn.execute!
165
+ end
166
+
167
+ should "reset the current flow to empty" do
168
+ assert !@conn.current_flow.empty?
169
+ @conn.execute!
170
+ assert @conn.current_flow.empty?
171
+ end
172
+
173
+ should "return the last task's return value" do
174
+ assert_equal :submit_form, @conn.execute!
175
+ end
176
+ end
177
+ end
178
+ end