arikui1911-hatenadiary 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/hatenadiary.rb +166 -0
  2. data/test/test_hatenadiary.rb +230 -0
  3. metadata +4 -4
@@ -0,0 +1,166 @@
1
+ #
2
+ # Distributes under The modified BSD license.
3
+ #
4
+ # Copyright (c) 2009 arikui <http://d.hatena.ne.jp/arikui/>
5
+ # All rights reserved.
6
+ #
7
+
8
+ require 'rubygems'
9
+ require 'www/mechanize'
10
+ require 'www/mechanize/util'
11
+ require 'nkf'
12
+
13
+ class << WWW::Mechanize::Util
14
+ org = instance_method(:html_unescape)
15
+ define_method(:html_unescape) do |s|
16
+ m = org.bind(self)
17
+ begin
18
+ m.call s
19
+ rescue ArgumentError
20
+ m.call s.force_encoding(NKF.guess(s))
21
+ end
22
+ end
23
+ end
24
+
25
+ module HatenaDiary
26
+ #
27
+ # Allocates Client object and makes it login, execute a received block,
28
+ # and then logout.
29
+ #
30
+ # :call-seq:
31
+ # login(username, password, proxy = nil){|client| ... }
32
+ #
33
+ def login(*args, &block)
34
+ Client.login(*args, &block)
35
+ end
36
+ module_function :login
37
+
38
+ class LoginError < RuntimeError
39
+ def set_account(username, password)
40
+ @username = username
41
+ @password = password
42
+ self
43
+ end
44
+
45
+ attr_reader :username
46
+ attr_reader :password
47
+ end
48
+
49
+ class Client
50
+ def self.mechanizer
51
+ @mechanizer ||= WWW::Mechanize
52
+ end
53
+
54
+ def self.mechanizer=(klass)
55
+ @mechanizer = klass
56
+ end
57
+
58
+ # Allocates Client object.
59
+ #
60
+ # If block given, login and execute a received block, and then logout ensurely.
61
+ #
62
+ # [username] Hatena ID
63
+ # [password] Password for _username_
64
+ # [proxy] Proxy configuration; [proxy_url, port_no] | nil
65
+ #
66
+ def self.login(username, password, proxy = nil, &block)
67
+ client = new(username, password)
68
+ client.set_proxy(*proxy) if proxy
69
+ return client unless block_given?
70
+ client.transaction(&block)
71
+ end
72
+
73
+ # Allocates Client object.
74
+ #
75
+ # [username] Hatena ID
76
+ # [password] Password for _username_
77
+ def initialize(username, password)
78
+ @agent = self.class.mechanizer.new
79
+ @username = username
80
+ @password = password
81
+ @current_account = nil
82
+ end
83
+
84
+ # Configure proxy.
85
+ def set_proxy(url, port)
86
+ @agent.set_proxy(url, port)
87
+ end
88
+
89
+ # Login and execute a received block, and then logout ensurely.
90
+ def transaction(username = nil, password = nil)
91
+ raise LocalJumpError, "no block given" unless block_given?
92
+ login(username, password)
93
+ begin
94
+ yield(self)
95
+ ensure
96
+ logout
97
+ end
98
+ end
99
+
100
+ # Returns a client itself was logined or not.
101
+ #
102
+ # -> true | false
103
+ def login?
104
+ @current_account ? true : false
105
+ end
106
+
107
+ # Does login.
108
+ #
109
+ # If _username_ or _password_ are invalid, raises HatenaDiary::LoginError .
110
+ def login(username = nil, password = nil)
111
+ username ||= @username
112
+ password ||= @password
113
+ form = @agent.get("https://www.hatena.ne.jp/login").forms.first
114
+ form["name"] = username
115
+ form["password"] = password
116
+ form["persistent"] = "true"
117
+ response = form.submit
118
+ @current_account = [username, password]
119
+ case response.title
120
+ when "Hatena" then response
121
+ when "Login - Hatena" then raise LoginError.new("login failure").set_account(username, password)
122
+ else raise Exception, '[BUG] must not happen (maybe cannot follow hatena spec)'
123
+ end
124
+ end
125
+
126
+ # Does logout if already logined.
127
+ def logout
128
+ return unless login?
129
+ @agent.get("https://www.hatena.ne.jp/logout")
130
+ account = @current_account
131
+ @current_account = nil
132
+ account
133
+ end
134
+
135
+ # Posts an entry to Hatena diary service.
136
+ #
137
+ # Raises HatenaDiary::LoginError unless logined.
138
+ def post(yyyy, mm, dd, title, body, trivial_p = false)
139
+ edit_page(yyyy, mm, dd, 0) do |form|
140
+ form["title"] = title
141
+ form["body"] = body
142
+ form["trivial"] = "true" if trivial_p
143
+ end
144
+ end
145
+
146
+ # Deletes an entry from Hatena diary service.
147
+ #
148
+ # Raises HatenaDiary::LoginError unless logined.
149
+ def delete(yyyy, mm, dd)
150
+ edit_page(yyyy, mm, dd, -1)
151
+ end
152
+
153
+ private
154
+
155
+ def edit_page(yyyy, mm, dd, form_index)
156
+ raise LoginError, "not login yet" unless login?
157
+ response = @agent.get("http://d.hatena.ne.jp/#{@current_account[0]}/edit?date=#{yyyy}#{mm}#{dd}")
158
+ form = response.forms.fetch(form_index)
159
+ form["year"] = "%04d" % yyyy
160
+ form["month"] = "%02d" % mm
161
+ form["day"] = "%02d" % dd
162
+ yield(form) if block_given?
163
+ form.submit
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,230 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'test/unit'
3
+ require 'hatenadiary'
4
+
5
+ module TU_CommonSetup
6
+ def common_setup
7
+ @username = 'HATENA_ID'
8
+ @password = 'PASSWORD'
9
+ @agent = TU_MechanizeMock.new
10
+ TU_MechanizeMock.queue.push @agent
11
+ TU_MechanizeMock::DUMMY_ACCOUNTS[@username] = @password
12
+ HatenaDiary::Client.mechanizer = TU_MechanizeMock
13
+ end
14
+ end
15
+
16
+ class TC_HatenaDiary < Test::Unit::TestCase
17
+ include TU_CommonSetup
18
+
19
+ def setup
20
+ common_setup
21
+ end
22
+
23
+ def test_login
24
+ HatenaDiary.login(@username, @password) do |client|
25
+ assert client.login?
26
+ end
27
+ end
28
+ end
29
+
30
+ class TC_HatenaDiary_Client_class < Test::Unit::TestCase
31
+ include TU_CommonSetup
32
+
33
+ def setup
34
+ common_setup
35
+ end
36
+
37
+ def test_login
38
+ HatenaDiary::Client.login(@username, @password) do |client|
39
+ assert_kind_of HatenaDiary::Client, client
40
+ assert client.login?
41
+ end
42
+ end
43
+
44
+ def test_login_without_block
45
+ client = HatenaDiary::Client.login(@username, @password)
46
+ assert_kind_of HatenaDiary::Client, client
47
+ assert !@agent.proxy
48
+ end
49
+
50
+ def test_login_without_block_with_proxy
51
+ client = HatenaDiary::Client.login(@username, @password, ['URL', 666])
52
+ assert_equal ['URL', 666], @agent.proxy
53
+ end
54
+ end
55
+
56
+ class TC_HatenaDiary_Client < Test::Unit::TestCase
57
+ include TU_CommonSetup
58
+
59
+ def setup
60
+ common_setup
61
+ @client = HatenaDiary::Client.new(@username, @password)
62
+ end
63
+
64
+ def test_set_proxy
65
+ @client.set_proxy('URL', 666)
66
+ assert_equal @agent.proxy, ['URL', 666]
67
+ end
68
+
69
+ def test_login_and_logout
70
+ assert !@client.login?
71
+ @client.login
72
+ assert @client.login?
73
+ @client.logout
74
+ assert !@client.login?
75
+ end
76
+
77
+ def test_login_failure
78
+ TU_MechanizeMock::DUMMY_ACCOUNTS.delete('no_one')
79
+ begin
80
+ @client.login 'no_one', 'password?'
81
+ flunk
82
+ rescue HatenaDiary::LoginError => ex
83
+ assert_kind_of HatenaDiary::LoginError, ex
84
+ assert_equal 'no_one', ex.username
85
+ assert_equal 'password?', ex.password
86
+ end
87
+ end
88
+
89
+ def test_login_if_hatena_changed
90
+ @agent.with_login_page_result_title 'jumbled page title :-)' do
91
+ @client.login
92
+ end
93
+ flunk
94
+ rescue Exception => ex
95
+ assert /must not happen/ =~ ex.message
96
+ end
97
+
98
+ def test_transaction
99
+ assert !@client.login?
100
+ @client.transaction do |client|
101
+ assert_same @client, client
102
+ assert @client.login?
103
+ end
104
+ assert !@client.login?
105
+ end
106
+
107
+ def test_transaction_without_block
108
+ assert !@client.login?
109
+ assert_raises LocalJumpError do
110
+ @client.transaction
111
+ end
112
+ assert !@client.login?
113
+ end
114
+
115
+ def test_post
116
+ @client.transaction do |client|
117
+ client.post 1234, 5, 6, "TITLE", "BODY\n"
118
+ end
119
+ h = @agent.latest_post_form
120
+ assert_equal "1234", h["year"]
121
+ assert_equal "05", h["month"]
122
+ assert_equal "06", h["day"]
123
+ assert_equal "TITLE", h["title"]
124
+ assert_equal "BODY\n", h["body"]
125
+ end
126
+
127
+ def test_post_trivial
128
+ @client.transaction do |client|
129
+ client.post 2000, 7, 8, "TITLE", "BODY\n", true
130
+ end
131
+ h = @agent.latest_post_form
132
+ assert_equal "edit", h[:form_id]
133
+ assert_equal "2000", h["year"]
134
+ assert_equal "07", h["month"]
135
+ assert_equal "08", h["day"]
136
+ assert_equal "TITLE", h["title"]
137
+ assert_equal "BODY\n", h["body"]
138
+ assert_equal "true", h["trivial"]
139
+ end
140
+
141
+ def test_post_without_login
142
+ assert_raises HatenaDiary::LoginError do
143
+ @client.post 1999, 5, 26, "TITLE", "BODY\n"
144
+ end
145
+ end
146
+
147
+ def test_delete
148
+ @client.transaction do |client|
149
+ client.delete 1234, 5, 6
150
+ end
151
+ h = @agent.latest_post_form
152
+ assert_equal "delete", h[:form_id]
153
+ assert_equal "1234", h["year"]
154
+ assert_equal "05", h["month"]
155
+ assert_equal "06", h["day"]
156
+ end
157
+
158
+ def test_delete_without_login
159
+ assert_raises HatenaDiary::LoginError do
160
+ @client.delete 2009, 8, 30
161
+ end
162
+ end
163
+ end
164
+
165
+ class TU_MechanizeMock
166
+ def self.queue
167
+ @queue ||= []
168
+ end
169
+
170
+ def self.new
171
+ queue.shift or super
172
+ end
173
+
174
+ def set_proxy(url, port)
175
+ @proxy = [url, port]
176
+ end
177
+
178
+ attr_reader :proxy
179
+ attr_reader :latest_post_form
180
+
181
+ def with_login_page_result_title(title, &block)
182
+ @preset_title = title
183
+ yield()
184
+ ensure
185
+ @preset_title = nil
186
+ end
187
+
188
+ DUMMY_ACCOUNTS = {}
189
+
190
+ def get(url)
191
+ case url
192
+ when "https://www.hatena.ne.jp/login"
193
+ forms = []
194
+ forms << create_form{|form|
195
+ form["persistent"] == "true" or throw :test_hatenadiary_mechanizemock_login_form_not_persistent
196
+ if @preset_title
197
+ Page.new(@preset_title)
198
+ else
199
+ Page.new(DUMMY_ACCOUNTS[form["name"]] == form["password"] ? "Hatena" : "Login - Hatena")
200
+ end
201
+ }
202
+ Page.new(nil, forms)
203
+ when "https://www.hatena.ne.jp/logout"
204
+ Page.new
205
+ when %r[\Ahttp://d.hatena.ne.jp/]
206
+ id, rest = $'.split('/', 2)
207
+ addr, query = rest.split('?', 2)
208
+ forms = [nil, nil, nil]
209
+ forms.unshift create_form{|form|
210
+ form[:form_id] = "edit"
211
+ @latest_post_form = form
212
+ }
213
+ forms.push create_form{|form|
214
+ form[:form_id] = "delete"
215
+ @latest_post_form = form
216
+ }
217
+ Page.new(nil, forms)
218
+ end
219
+ end
220
+
221
+ Page = Struct.new(:title, :forms)
222
+
223
+ def create_form(&block)
224
+ form = { :__submitter__ => block }
225
+ def form.submit
226
+ fetch(:__submitter__).call(self)
227
+ end
228
+ form
229
+ end
230
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arikui1911-hatenadiary
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - arikui
@@ -35,8 +35,8 @@ files:
35
35
  - README
36
36
  - LICENSE
37
37
  - Rakefile
38
- - test
39
- - lib
38
+ - test/test_hatenadiary.rb
39
+ - lib/hatenadiary.rb
40
40
  has_rdoc: false
41
41
  homepage: http://wiki.github.com/arikui1911/hatenadiary
42
42
  post_install_message:
@@ -73,4 +73,4 @@ signing_key:
73
73
  specification_version: 3
74
74
  summary: It is a library provides a client for Hatena Diary to post and delete blog entries.
75
75
  test_files:
76
- - test
76
+ - test/test_hatenadiary.rb