session_test.rb 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. # frozen_string_literal: true
  2. require 'test_helper'
  3. require 'timecop'
  4. class SessionTest < Test::Unit::TestCase
  5. SECONDS_IN_A_DAY = 24 * 60 * 60
  6. def setup
  7. super
  8. ShopifyAPI::Session.secret = 'secret'
  9. end
  10. test "not be valid without a url" do
  11. session = ShopifyAPI::Session.new(domain: nil, token: "any-token", api_version: any_api_version)
  12. assert_not(session.valid?)
  13. end
  14. test "not be valid without token" do
  15. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: nil, api_version: any_api_version)
  16. assert_not(session.valid?)
  17. end
  18. test "not be valid without an API version" do
  19. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: "any-token", api_version: nil)
  20. assert_not(session.valid?)
  21. session = ShopifyAPI::Session.new(
  22. domain: "testshop.myshopify.com", token: "any-token", api_version: ShopifyAPI::ApiVersion::NullVersion
  23. )
  24. assert_not(session.valid?)
  25. end
  26. test "default to base API version" do
  27. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: "any-token")
  28. assert(session.valid?)
  29. assert_equal(session.api_version, ShopifyAPI::Base.api_version)
  30. end
  31. test "can override the base API version" do
  32. different_api_version = '2020-01'
  33. session = ShopifyAPI::Session.new(
  34. domain: "testshop.myshopify.com", token: "any-token", api_version: different_api_version
  35. )
  36. assert(session.valid?)
  37. assert_equal(session.api_version, ShopifyAPI::ApiVersion.find_version(different_api_version))
  38. end
  39. test "be valid with any token, any url and version" do
  40. session = ShopifyAPI::Session.new(
  41. domain: "testshop.myshopify.com",
  42. token: "any-token",
  43. api_version: any_api_version
  44. )
  45. assert(session.valid?)
  46. end
  47. test "not raise error without params" do
  48. assert_nothing_raised do
  49. ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: "any-token", api_version: any_api_version)
  50. end
  51. end
  52. test "ignore everything but the subdomain in the shop" do
  53. assert_equal(
  54. "https://testshop.myshopify.com",
  55. ShopifyAPI::Session.new(
  56. domain: "http://user:pass@testshop.notshopify.net/path",
  57. token: "any-token",
  58. api_version: any_api_version
  59. ).site
  60. )
  61. end
  62. test "append the myshopify domain if not given" do
  63. assert_equal(
  64. "https://testshop.myshopify.com",
  65. ShopifyAPI::Session.new(domain: "testshop", token: "any-token", api_version: any_api_version).site
  66. )
  67. end
  68. test "not raise error without params" do
  69. assert_nothing_raised do
  70. ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: "any-token", api_version: any_api_version)
  71. end
  72. end
  73. test "raise error if params passed but signature omitted" do
  74. assert_raises(ShopifyAPI::ValidationException) do
  75. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: nil, api_version: any_api_version)
  76. session.request_token({ 'code' => 'any-code' })
  77. end
  78. end
  79. test "setup api_key and secret for all sessions" do
  80. ShopifyAPI::Session.setup(api_key: "My test key", secret: "My test secret")
  81. assert_equal("My test key", ShopifyAPI::Session.api_key)
  82. assert_equal("My test secret", ShopifyAPI::Session.secret)
  83. end
  84. test "#temp reset ShopifyAPI::Base values to original value" do
  85. session1 = ShopifyAPI::Session.new(domain: 'fakeshop.myshopify.com', token: 'token1', api_version: '2019-01')
  86. ShopifyAPI::Base.user = 'foo'
  87. ShopifyAPI::Base.password = 'bar'
  88. ShopifyAPI::Base.activate_session(session1)
  89. ShopifyAPI::Session.temp(domain: "testshop.myshopify.com", token: "any-token", api_version: :unstable) do
  90. @assigned_site = ShopifyAPI::Base.site
  91. @assigned_version = ShopifyAPI::Base.api_version
  92. @assigned_user = ShopifyAPI::Base.user
  93. @assigned_password = ShopifyAPI::Base.password
  94. end
  95. assert_equal('https://testshop.myshopify.com', @assigned_site.to_s)
  96. assert_equal('https://fakeshop.myshopify.com', ShopifyAPI::Base.site.to_s)
  97. assert_equal(ShopifyAPI::ApiVersion.new(handle: :unstable), @assigned_version)
  98. assert_equal(ShopifyAPI::ApiVersion.new(handle: '2019-01'), ShopifyAPI::Base.api_version)
  99. assert_nil(@assigned_user)
  100. assert_equal('foo', ShopifyAPI::Base.user)
  101. assert_nil(@assigned_password)
  102. assert_equal('bar', ShopifyAPI::Base.password)
  103. end
  104. test "#temp does not use basic auth values from Base.site" do
  105. ShopifyAPI::Base.site = 'https://user:pass@fakeshop.myshopify.com'
  106. ShopifyAPI::Session.temp(domain: "testshop.myshopify.com", token: "any-token", api_version: :unstable) do
  107. @assigned_site = ShopifyAPI::Base.site
  108. @assigned_user = ShopifyAPI::Base.user
  109. @assigned_password = ShopifyAPI::Base.password
  110. end
  111. assert_equal('https://testshop.myshopify.com', @assigned_site.to_s)
  112. assert_equal('https://fakeshop.myshopify.com', ShopifyAPI::Base.site.to_s)
  113. assert_nil(@assigned_user)
  114. assert_equal('user', ShopifyAPI::Base.user)
  115. assert_nil(@assigned_password)
  116. assert_equal('pass', ShopifyAPI::Base.password)
  117. end
  118. test "#with_session activates the session for the duration of the block" do
  119. session1 = ShopifyAPI::Session.new(domain: 'fakeshop.myshopify.com', token: 'token1', api_version: '2019-01')
  120. ShopifyAPI::Base.activate_session(session1)
  121. other_session = ShopifyAPI::Session.new(
  122. domain: "testshop.myshopify.com",
  123. token: "any-token",
  124. api_version: :unstable
  125. )
  126. ShopifyAPI::Session.with_session(other_session) do
  127. @assigned_site = ShopifyAPI::Base.site
  128. @assigned_version = ShopifyAPI::Base.api_version
  129. end
  130. assert_equal('https://testshop.myshopify.com', @assigned_site.to_s)
  131. assert_equal('https://fakeshop.myshopify.com', ShopifyAPI::Base.site.to_s)
  132. assert_equal(ShopifyAPI::ApiVersion.new(handle: :unstable), @assigned_version)
  133. assert_equal(ShopifyAPI::ApiVersion.new(handle: '2019-01'), ShopifyAPI::Base.api_version)
  134. end
  135. test "#with_session resets the activated session even if there an exception during the block" do
  136. session1 = ShopifyAPI::Session.new(domain: 'fakeshop.myshopify.com', token: 'token1', api_version: '2019-01')
  137. ShopifyAPI::Base.activate_session(session1)
  138. other_session = ShopifyAPI::Session.new(
  139. domain: "testshop.myshopify.com",
  140. token: "any-token",
  141. api_version: :unstable
  142. )
  143. assert_raises(StandardError) do
  144. ShopifyAPI::Session.with_session(other_session) { raise StandardError, "" }
  145. end
  146. assert_equal('https://fakeshop.myshopify.com', ShopifyAPI::Base.site.to_s)
  147. assert_equal(ShopifyAPI::ApiVersion.new(handle: '2019-01'), ShopifyAPI::Base.api_version)
  148. end
  149. test "#with_version will adjust the actvated api version for the duration of the block" do
  150. session1 = ShopifyAPI::Session.new(domain: 'fakeshop.myshopify.com', token: 'token1', api_version: '2019-01')
  151. ShopifyAPI::Base.activate_session(session1)
  152. ShopifyAPI::Session.with_version(:unstable) do
  153. @assigned_site = ShopifyAPI::Base.site
  154. @assigned_version = ShopifyAPI::Base.api_version
  155. end
  156. assert_equal('https://fakeshop.myshopify.com', @assigned_site.to_s)
  157. assert_equal('https://fakeshop.myshopify.com', ShopifyAPI::Base.site.to_s)
  158. assert_equal(ShopifyAPI::ApiVersion.new(handle: :unstable), @assigned_version)
  159. assert_equal(ShopifyAPI::ApiVersion.new(handle: '2019-01'), ShopifyAPI::Base.api_version)
  160. end
  161. test "create_permission_url requires redirect_uri" do
  162. ShopifyAPI::Session.setup(api_key: "My_test_key", secret: "My test secret")
  163. session = ShopifyAPI::Session.new(
  164. domain: 'http://localhost.myshopify.com',
  165. token: 'any-token',
  166. api_version: any_api_version
  167. )
  168. scope = ["write_products"]
  169. assert_raises(ArgumentError) do
  170. session.create_permission_url(scope)
  171. end
  172. end
  173. test "create_permission_url returns correct url with single scope and redirect uri" do
  174. ShopifyAPI::Session.setup(api_key: "My_test_key", secret: "My test secret")
  175. session = ShopifyAPI::Session.new(
  176. domain: 'http://localhost.myshopify.com',
  177. token: 'any-token',
  178. api_version: any_api_version
  179. )
  180. scope = ["write_products"]
  181. permission_url = session.create_permission_url(scope, "http://my_redirect_uri.com")
  182. assert_equal(
  183. "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&" \
  184. "scope=write_products&redirect_uri=http://my_redirect_uri.com",
  185. permission_url
  186. )
  187. end
  188. test "create_permission_url returns correct url with dual scope" do
  189. ShopifyAPI::Session.setup(api_key: "My_test_key", secret: "My test secret")
  190. session = ShopifyAPI::Session.new(
  191. domain: 'http://localhost.myshopify.com',
  192. token: 'any-token',
  193. api_version: any_api_version
  194. )
  195. scope = ["write_products", "write_customers"]
  196. permission_url = session.create_permission_url(scope, "http://my_redirect_uri.com")
  197. assert_equal(
  198. "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&" \
  199. "scope=write_products,write_customers&redirect_uri=http://my_redirect_uri.com",
  200. permission_url
  201. )
  202. end
  203. test "create_permission_url returns correct url with no scope" do
  204. ShopifyAPI::Session.setup(api_key: "My_test_key", secret: "My test secret")
  205. session = ShopifyAPI::Session.new(
  206. domain: 'http://localhost.myshopify.com',
  207. token: 'any-token',
  208. api_version: any_api_version
  209. )
  210. scope = []
  211. permission_url = session.create_permission_url(scope, "http://my_redirect_uri.com")
  212. assert_equal(
  213. "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&" \
  214. "scope=&redirect_uri=http://my_redirect_uri.com",
  215. permission_url
  216. )
  217. end
  218. test "create_permission_url returns correct url with state" do
  219. ShopifyAPI::Session.setup(api_key: "My_test_key", secret: "My test secret")
  220. session = ShopifyAPI::Session.new(
  221. domain: 'http://localhost.myshopify.com',
  222. token: 'any-token',
  223. api_version: any_api_version
  224. )
  225. scope = []
  226. permission_url = session.create_permission_url(scope, "http://my_redirect_uri.com", state: "My nonce")
  227. assert_equal(
  228. "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&" \
  229. "scope=&redirect_uri=http://my_redirect_uri.com&state=My%20nonce",
  230. permission_url
  231. )
  232. end
  233. test "raise exception if code invalid in request token" do
  234. ShopifyAPI::Session.setup(api_key: "My test key", secret: "My test secret")
  235. session = ShopifyAPI::Session.new(
  236. domain: 'http://localhost.myshopify.com',
  237. token: nil,
  238. api_version: any_api_version
  239. )
  240. fake(
  241. nil,
  242. url: 'https://localhost.myshopify.com/admin/oauth/access_token',
  243. method: :post,
  244. status: 404,
  245. body: '{"error" : "invalid_request"}'
  246. )
  247. assert_raises(ShopifyAPI::ValidationException) do
  248. session.request_token(code: "bad-code")
  249. end
  250. assert_equal(false, session.valid?)
  251. end
  252. test "return site for session" do
  253. session = ShopifyAPI::Session.new(
  254. domain: "testshop.myshopify.com",
  255. token: "any-token",
  256. api_version: any_api_version
  257. )
  258. assert_equal("https://testshop.myshopify.com", session.site)
  259. end
  260. test "return_token_if_signature_is_valid" do
  261. api_version = any_api_version
  262. fake(
  263. nil,
  264. url: "https://testshop.myshopify.com/admin/oauth/access_token",
  265. method: :post,
  266. body: '{"access_token":"any-token"}'
  267. )
  268. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: nil, api_version: api_version)
  269. params = { code: 'any-code', timestamp: Time.now }
  270. token = session.request_token(params.merge(hmac: generate_signature(params)))
  271. assert_equal("any-token", token)
  272. assert_equal("any-token", session.token)
  273. end
  274. test "extra parameters are stored in session" do
  275. api_version = ShopifyAPI::ApiVersion.new(handle: :unstable)
  276. fake(
  277. nil,
  278. url: "https://testshop.myshopify.com/admin/oauth/access_token",
  279. method: :post,
  280. body: '{"access_token":"any-token","foo":"example"}'
  281. )
  282. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: nil, api_version: api_version)
  283. params = { code: 'any-code', timestamp: Time.now }
  284. assert(session.request_token(params.merge(hmac: generate_signature(params))))
  285. assert_equal({ "foo" => "example" }, session.extra)
  286. end
  287. test "expires_in is automatically converted in expires_at" do
  288. api_version = any_api_version
  289. fake(
  290. nil,
  291. url: "https://testshop.myshopify.com/admin/oauth/access_token",
  292. method: :post,
  293. body: '{"access_token":"any-token","expires_in":86393}'
  294. )
  295. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: nil, api_version: api_version)
  296. Timecop.freeze do
  297. params = { code: 'any-code', timestamp: Time.now }
  298. assert(session.request_token(params.merge(hmac: generate_signature(params))))
  299. expires_at = Time.now.utc + 86393
  300. assert_equal({ "expires_at" => expires_at.to_i }, session.extra)
  301. assert(session.expires_at.is_a?(Time))
  302. assert_equal(expires_at.to_i, session.expires_at.to_i)
  303. assert_equal(86393, session.expires_in)
  304. assert_equal(false, session.expired?)
  305. Timecop.travel(session.expires_at) do
  306. assert_equal(true, session.expired?)
  307. end
  308. end
  309. end
  310. test "raise error if signature does not match expected" do
  311. params = { code: "any-code", timestamp: Time.now }
  312. signature = generate_signature(params)
  313. params[:foo] = 'world'
  314. assert_raises(ShopifyAPI::ValidationException) do
  315. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: nil, api_version: any_api_version)
  316. session.request_token(params.merge(hmac: signature))
  317. end
  318. end
  319. test "raise error if timestamp is too old" do
  320. params = { code: "any-code", timestamp: Time.now - 2 * SECONDS_IN_A_DAY }
  321. signature = generate_signature(params)
  322. params[:foo] = 'world'
  323. assert_raises(ShopifyAPI::ValidationException) do
  324. session = ShopifyAPI::Session.new(domain: "testshop.myshopify.com", token: nil, api_version: any_api_version)
  325. session.request_token(params.merge(hmac: signature))
  326. end
  327. end
  328. test "return true when the signature is valid and the keys of params are strings" do
  329. params = { 'code' => 'any-code', 'timestamp' => Time.now }
  330. params[:hmac] = generate_signature(params)
  331. assert_equal(true, ShopifyAPI::Session.validate_signature(params))
  332. end
  333. test "return true when validating signature of params with ampersand and equal sign characters" do
  334. ShopifyAPI::Session.secret = 'secret'
  335. params = { 'a' => '1&b=2', 'c=3&d' => '4' }
  336. to_sign = 'a=1%26b=2&c%3D3%26d=4'
  337. params[:hmac] = generate_signature(to_sign)
  338. assert_equal(true, ShopifyAPI::Session.validate_signature(params))
  339. end
  340. test "return true when validating signature of params with percent sign characters" do
  341. ShopifyAPI::Session.secret = 'secret'
  342. params = { 'a%3D1%26b' => '2%26c%3D3' }
  343. to_sign = 'a%253D1%2526b=2%2526c%253D3'
  344. params[:hmac] = generate_signature(to_sign)
  345. assert_equal(true, ShopifyAPI::Session.validate_signature(params))
  346. end
  347. test "url is aliased to domain to minimize the upgrade changes" do
  348. session = ShopifyAPI::Session.new(
  349. domain: "http://testshop.myshopify.com",
  350. token: "any-token",
  351. api_version: any_api_version
  352. )
  353. assert_equal('testshop.myshopify.com', session.url)
  354. end
  355. test "#hash returns the same value for equal Sessions" do
  356. session = ShopifyAPI::Session.new(
  357. domain: "http://testshop.myshopify.com",
  358. token: "any-token",
  359. api_version: '2019-01',
  360. extra: { foo: "bar" }
  361. )
  362. other_session = ShopifyAPI::Session.new(
  363. domain: "http://testshop.myshopify.com",
  364. token: "any-token",
  365. api_version: '2019-01',
  366. extra: { foo: "bar" }
  367. )
  368. assert_equal(session.hash, other_session.hash)
  369. end
  370. test "equality verifies domain" do
  371. session = ShopifyAPI::Session.new(
  372. domain: "http://testshop.myshopify.com",
  373. token: "any-token",
  374. api_version: '2019-01',
  375. extra: { foo: "bar" }
  376. )
  377. other_session = ShopifyAPI::Session.new(
  378. domain: "http://testshop.myshopify.com",
  379. token: "any-token",
  380. api_version: '2019-01',
  381. extra: { foo: "bar" }
  382. )
  383. different_session = ShopifyAPI::Session.new(
  384. domain: "http://another_testshop.myshopify.com",
  385. token: "any-token",
  386. api_version: '2019-01',
  387. extra: { foo: "bar" }
  388. )
  389. assert_equal(session, other_session)
  390. refute_equal(session, different_session)
  391. end
  392. test "equality verifies token" do
  393. session = ShopifyAPI::Session.new(
  394. domain: "http://testshop.myshopify.com",
  395. token: "any-token",
  396. api_version: '2019-01',
  397. extra: { foo: "bar" }
  398. )
  399. different_session = ShopifyAPI::Session.new(
  400. domain: "http://testshop.myshopify.com",
  401. token: "very-different-token",
  402. api_version: '2019-01',
  403. extra: { foo: "bar" }
  404. )
  405. refute_equal(session, different_session)
  406. end
  407. test "equality verifies api_version" do
  408. session = ShopifyAPI::Session.new(
  409. domain: "http://testshop.myshopify.com",
  410. token: "any-token",
  411. api_version: '2019-01',
  412. extra: { foo: "bar" }
  413. )
  414. different_session = ShopifyAPI::Session.new(
  415. domain: "http://testshop.myshopify.com",
  416. token: "any-token",
  417. api_version: :unstable,
  418. extra: { foo: "bar" }
  419. )
  420. refute_equal(session, different_session)
  421. end
  422. test "equality verifies extra" do
  423. session = ShopifyAPI::Session.new(
  424. domain: "http://testshop.myshopify.com",
  425. token: "any-token",
  426. api_version: '2019-01',
  427. extra: { foo: "bar" }
  428. )
  429. different_session = ShopifyAPI::Session.new(
  430. domain: "http://testshop.myshopify.com",
  431. token: "any-token",
  432. api_version: '2019-01',
  433. extra: { bar: "other-bar" }
  434. )
  435. refute_equal(session, different_session)
  436. end
  437. test "equality verifies other is a Session" do
  438. session = ShopifyAPI::Session.new(
  439. domain: "http://testshop.myshopify.com",
  440. token: "any-token",
  441. api_version: '2019-01',
  442. extra: { foo: "bar" }
  443. )
  444. different_session = nil
  445. refute_equal(session, different_session)
  446. end
  447. test "#eql? and #hash are implemented" do
  448. session = ShopifyAPI::Session.new(
  449. domain: "http://testshop.myshopify.com",
  450. token: "any-token",
  451. api_version: '2019-01',
  452. extra: { foo: "bar" }
  453. )
  454. other_session = ShopifyAPI::Session.new(
  455. domain: "http://testshop.myshopify.com",
  456. token: "any-token",
  457. api_version: '2019-01',
  458. extra: { foo: "bar" }
  459. )
  460. different_session = ShopifyAPI::Session.new(
  461. domain: "http://another_testshop.myshopify.com",
  462. token: "any-token",
  463. api_version: '2019-01',
  464. extra: { foo: "bar" }
  465. )
  466. assert_equal([session, different_session], [session, other_session, different_session].uniq)
  467. end
  468. private
  469. def make_sorted_params(params)
  470. params.with_indifferent_access.except(
  471. :signature, :hmac, :action, :controller
  472. ).collect { |k, v| "#{k}=#{v}" }.sort.join('&')
  473. end
  474. def generate_signature(params)
  475. params = make_sorted_params(params) if params.is_a?(Hash)
  476. OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), ShopifyAPI::Session.secret, params)
  477. end
  478. def any_api_version
  479. version_name = ['2019-01', :unstable].sample(1).first
  480. ShopifyAPI::ApiVersion.find_version(version_name)
  481. end
  482. end