cache_spec.rb 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. # frozen_string_literal: true
  2. require 'rails_helper'
  3. module TestEndpoints
  4. # Endpoints that do not include authorization-dependent results
  5. # and should be cacheable no matter what.
  6. ALWAYS_CACHED = %w(
  7. /.well-known/host-meta
  8. /.well-known/nodeinfo
  9. /nodeinfo/2.0
  10. /manifest
  11. /custom.css
  12. /actor
  13. /api/v1/instance/extended_description
  14. /api/v1/instance/rules
  15. /api/v1/instance/peers
  16. /api/v1/instance
  17. /api/v2/instance
  18. ).freeze
  19. # Endpoints that should be cachable when accessed anonymously but have a Vary
  20. # on Cookie to prevent logged-in users from getting values from logged-out cache.
  21. COOKIE_DEPENDENT_CACHABLE = %w(
  22. /
  23. /explore
  24. /public
  25. /about
  26. /privacy-policy
  27. /directory
  28. /@alice
  29. /@alice/110224538612341312
  30. /deck/home
  31. ).freeze
  32. # Endpoints that should be cachable when accessed anonymously but have a Vary
  33. # on Authorization to prevent logged-in users from getting values from logged-out cache.
  34. AUTHORIZATION_DEPENDENT_CACHABLE = %w(
  35. /api/v1/accounts/lookup?acct=alice
  36. /api/v1/statuses/110224538612341312
  37. /api/v1/statuses/110224538612341312/context
  38. /api/v1/polls/12345
  39. /api/v1/trends/statuses
  40. /api/v1/directory
  41. ).freeze
  42. # Private status that should only be returned with to a valid signature from
  43. # a specific user.
  44. # Should never be cached.
  45. REQUIRE_SIGNATURE = %w(
  46. /users/alice/statuses/110224538643211312
  47. ).freeze
  48. # Pages only available to logged-in users.
  49. # Should never be cached.
  50. REQUIRE_LOGIN = %w(
  51. /settings/preferences/appearance
  52. /settings/profile
  53. /settings/featured_tags
  54. /settings/export
  55. /relationships
  56. /filters
  57. /statuses_cleanup
  58. /auth/edit
  59. /oauth/authorized_applications
  60. /admin/dashboard
  61. ).freeze
  62. # API endpoints only available to logged-in users.
  63. # Should never be cached.
  64. REQUIRE_TOKEN = %w(
  65. /api/v1/announcements
  66. /api/v1/timelines/home
  67. /api/v1/notifications
  68. /api/v1/bookmarks
  69. /api/v1/favourites
  70. /api/v1/follow_requests
  71. /api/v1/conversations
  72. /api/v1/statuses/110224538643211312
  73. /api/v1/statuses/110224538643211312/context
  74. /api/v1/lists
  75. /api/v2/filters
  76. ).freeze
  77. # Pages that are only shown to logged-out users, and should never get cached
  78. # because of CSRF protection.
  79. REQUIRE_LOGGED_OUT = %w(
  80. /invite/abcdef
  81. /auth/sign_in
  82. /auth/sign_up
  83. /auth/password/new
  84. /auth/confirmation/new
  85. ).freeze
  86. # Non-exhaustive list of endpoints that feature language-dependent results
  87. # and thus need to have a Vary on Accept-Language
  88. LANGUAGE_DEPENDENT = %w(
  89. /
  90. /explore
  91. /about
  92. /api/v1/trends/statuses
  93. ).freeze
  94. module AuthorizedFetch
  95. # Endpoints that require a signature with AUTHORIZED_FETCH and LIMITED_FEDERATION_MODE
  96. # and thus should not be cached in those modes.
  97. REQUIRE_SIGNATURE = %w(
  98. /users/alice
  99. ).freeze
  100. end
  101. module DisabledAnonymousAPI
  102. # Endpoints that require a signature with DISALLOW_UNAUTHENTICATED_API_ACCESS
  103. # and thus should not be cached in this mode.
  104. REQUIRE_TOKEN = %w(
  105. /api/v1/custom_emojis
  106. ).freeze
  107. end
  108. end
  109. describe 'Caching behavior' do
  110. shared_examples 'cachable response' do
  111. it 'does not set cookies' do
  112. expect(response.cookies).to be_empty
  113. end
  114. it 'sets public cache control', :aggregate_failures do
  115. # expect(response.cache_control[:max_age]&.to_i).to be_positive
  116. expect(response.cache_control[:public]).to be_truthy
  117. expect(response.cache_control[:private]).to be_falsy
  118. expect(response.cache_control[:no_store]).to be_falsy
  119. expect(response.cache_control[:no_cache]).to be_falsy
  120. end
  121. end
  122. shared_examples 'non-cacheable response' do
  123. it 'sets private cache control' do
  124. expect(response.cache_control[:private]).to be_truthy
  125. expect(response.cache_control[:no_store]).to be_truthy
  126. end
  127. end
  128. shared_examples 'non-cacheable error' do
  129. it 'does not return HTTP success and does not have cache headers', :aggregate_failures do
  130. expect(response).to_not have_http_status(200)
  131. expect(response.cache_control[:public]).to be_falsy
  132. end
  133. end
  134. shared_examples 'language-dependent' do
  135. it 'has a Vary on Accept-Language' do
  136. expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept-language')
  137. end
  138. end
  139. # Enable CSRF protection like it is in production, as it can cause cookies
  140. # to be set and thus mess with cache.
  141. around do |example|
  142. old = ActionController::Base.allow_forgery_protection
  143. ActionController::Base.allow_forgery_protection = true
  144. example.run
  145. ActionController::Base.allow_forgery_protection = old
  146. end
  147. let(:alice) { Fabricate(:account, username: 'alice') }
  148. let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Moderator')) }
  149. before do
  150. status = Fabricate(:status, account: alice, id: '110224538612341312')
  151. Fabricate(:status, account: alice, id: '110224538643211312', visibility: :private)
  152. Fabricate(:invite, code: 'abcdef')
  153. Fabricate(:poll, status: status, account: alice, id: '12345')
  154. user.account.follow!(alice)
  155. end
  156. context 'when anonymously accessed' do
  157. describe '/users/alice' do
  158. it 'redirects with proper cache header', :aggregate_failures do
  159. get '/users/alice'
  160. expect(response).to redirect_to('/@alice')
  161. expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept')
  162. end
  163. end
  164. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  165. describe endpoint do
  166. before { get endpoint }
  167. it_behaves_like 'cachable response'
  168. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  169. end
  170. end
  171. TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint|
  172. describe endpoint do
  173. before { get endpoint }
  174. it_behaves_like 'cachable response'
  175. it 'has a Vary on Cookie' do
  176. expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('cookie')
  177. end
  178. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  179. end
  180. end
  181. TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
  182. describe endpoint do
  183. before { get endpoint }
  184. it_behaves_like 'cachable response'
  185. it 'has a Vary on Authorization' do
  186. expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization')
  187. end
  188. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  189. end
  190. end
  191. TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
  192. describe endpoint do
  193. before { get endpoint }
  194. it_behaves_like 'non-cacheable response'
  195. end
  196. end
  197. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::REQUIRE_LOGIN + TestEndpoints::REQUIRE_TOKEN).each do |endpoint|
  198. describe endpoint do
  199. before { get endpoint }
  200. it_behaves_like 'non-cacheable error'
  201. end
  202. end
  203. describe '/api/v1/instance/domain_blocks' do
  204. around do |example|
  205. old_setting = Setting.show_domain_blocks
  206. Setting.show_domain_blocks = show_domain_blocks
  207. example.run
  208. Setting.show_domain_blocks = old_setting
  209. end
  210. before { get '/api/v1/instance/domain_blocks' }
  211. context 'when set to be publicly-available' do
  212. let(:show_domain_blocks) { 'all' }
  213. it_behaves_like 'cachable response'
  214. end
  215. context 'when allowed for local users only' do
  216. let(:show_domain_blocks) { 'users' }
  217. it_behaves_like 'non-cacheable error'
  218. end
  219. context 'when disabled' do
  220. let(:show_domain_blocks) { 'disabled' }
  221. it_behaves_like 'non-cacheable error'
  222. end
  223. end
  224. end
  225. context 'when logged in' do
  226. before do
  227. sign_in user, scope: :user
  228. # Unfortunately, devise's `sign_in` helper causes the `session` to be
  229. # loaded in the next request regardless of whether it's actually accessed
  230. # by the client code.
  231. #
  232. # So, we make an extra query to clear issue a session cookie instead.
  233. #
  234. # A less resource-intensive way to deal with that would be to generate the
  235. # session cookie manually, but this seems pretty involved.
  236. get '/'
  237. end
  238. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  239. describe endpoint do
  240. before { get endpoint }
  241. it_behaves_like 'cachable response'
  242. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  243. end
  244. end
  245. TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint|
  246. describe endpoint do
  247. before { get endpoint }
  248. it_behaves_like 'non-cacheable response'
  249. it 'has a Vary on Cookie' do
  250. expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('cookie')
  251. end
  252. end
  253. end
  254. TestEndpoints::REQUIRE_LOGIN.each do |endpoint|
  255. describe endpoint do
  256. before { get endpoint }
  257. it_behaves_like 'non-cacheable response'
  258. it 'returns HTTP success' do
  259. expect(response).to have_http_status(200)
  260. end
  261. end
  262. end
  263. TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
  264. describe endpoint do
  265. before { get endpoint }
  266. it_behaves_like 'non-cacheable error'
  267. end
  268. end
  269. end
  270. context 'with an auth token' do
  271. let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
  272. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  273. describe endpoint do
  274. before do
  275. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  276. end
  277. it_behaves_like 'cachable response'
  278. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  279. end
  280. end
  281. TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
  282. describe endpoint do
  283. before do
  284. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  285. end
  286. it_behaves_like 'non-cacheable response'
  287. it 'has a Vary on Authorization' do
  288. expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization')
  289. end
  290. end
  291. end
  292. (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN).each do |endpoint|
  293. describe endpoint do
  294. before do
  295. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  296. end
  297. it_behaves_like 'non-cacheable response'
  298. it 'returns HTTP success' do
  299. expect(response).to have_http_status(200)
  300. end
  301. end
  302. end
  303. describe '/api/v1/instance/domain_blocks' do
  304. around do |example|
  305. old_setting = Setting.show_domain_blocks
  306. Setting.show_domain_blocks = show_domain_blocks
  307. example.run
  308. Setting.show_domain_blocks = old_setting
  309. end
  310. before do
  311. get '/api/v1/instance/domain_blocks', headers: { 'Authorization' => "Bearer #{token.token}" }
  312. end
  313. context 'when set to be publicly-available' do
  314. let(:show_domain_blocks) { 'all' }
  315. it_behaves_like 'cachable response'
  316. end
  317. context 'when allowed for local users only' do
  318. let(:show_domain_blocks) { 'users' }
  319. it_behaves_like 'non-cacheable response'
  320. it 'returns HTTP success' do
  321. expect(response).to have_http_status(200)
  322. end
  323. end
  324. context 'when disabled' do
  325. let(:show_domain_blocks) { 'disabled' }
  326. it_behaves_like 'non-cacheable error'
  327. end
  328. end
  329. end
  330. context 'with a Signature header' do
  331. let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
  332. let(:dummy_signature) { 'dummy-signature' }
  333. before do
  334. remote_actor.follow!(alice)
  335. end
  336. describe '/actor' do
  337. before do
  338. get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  339. end
  340. it_behaves_like 'cachable response'
  341. it 'returns HTTP success' do
  342. expect(response).to have_http_status(200)
  343. end
  344. end
  345. TestEndpoints::REQUIRE_SIGNATURE.each do |endpoint|
  346. describe endpoint do
  347. before do
  348. get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  349. end
  350. it_behaves_like 'non-cacheable response'
  351. it 'returns HTTP success' do
  352. expect(response).to have_http_status(200)
  353. end
  354. end
  355. end
  356. end
  357. context 'when enabling AUTHORIZED_FETCH mode' do
  358. around do |example|
  359. ClimateControl.modify AUTHORIZED_FETCH: 'true' do
  360. example.run
  361. end
  362. end
  363. context 'when not providing a Signature' do
  364. describe '/actor' do
  365. before do
  366. get '/actor', headers: { 'Accept' => 'application/activity+json' }
  367. end
  368. it_behaves_like 'cachable response'
  369. it 'returns HTTP success' do
  370. expect(response).to have_http_status(200)
  371. end
  372. end
  373. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  374. describe endpoint do
  375. before do
  376. get endpoint, headers: { 'Accept' => 'application/activity+json' }
  377. end
  378. it_behaves_like 'non-cacheable error'
  379. end
  380. end
  381. end
  382. context 'when providing a Signature' do
  383. let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
  384. let(:dummy_signature) { 'dummy-signature' }
  385. before do
  386. remote_actor.follow!(alice)
  387. end
  388. describe '/actor' do
  389. before do
  390. get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  391. end
  392. it_behaves_like 'cachable response'
  393. it 'returns HTTP success' do
  394. expect(response).to have_http_status(200)
  395. end
  396. end
  397. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  398. describe endpoint do
  399. before do
  400. get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  401. end
  402. it_behaves_like 'non-cacheable response'
  403. it 'returns HTTP success' do
  404. expect(response).to have_http_status(200)
  405. end
  406. end
  407. end
  408. end
  409. end
  410. context 'when enabling LIMITED_FEDERATION_MODE mode' do
  411. around do |example|
  412. ClimateControl.modify LIMITED_FEDERATION_MODE: 'true' do
  413. old_limited_federation_mode = Rails.configuration.x.limited_federation_mode
  414. Rails.configuration.x.limited_federation_mode = true
  415. example.run
  416. Rails.configuration.x.limited_federation_mode = old_limited_federation_mode
  417. end
  418. end
  419. context 'when not providing a Signature' do
  420. describe '/actor' do
  421. before do
  422. get '/actor', headers: { 'Accept' => 'application/activity+json' }
  423. end
  424. it_behaves_like 'cachable response'
  425. it 'returns HTTP success' do
  426. expect(response).to have_http_status(200)
  427. end
  428. end
  429. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  430. describe endpoint do
  431. before do
  432. get endpoint, headers: { 'Accept' => 'application/activity+json' }
  433. end
  434. it_behaves_like 'non-cacheable error'
  435. end
  436. end
  437. end
  438. context 'when providing a Signature from an allowed domain' do
  439. let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
  440. let(:dummy_signature) { 'dummy-signature' }
  441. before do
  442. DomainAllow.create!(domain: remote_actor.domain)
  443. remote_actor.follow!(alice)
  444. end
  445. describe '/actor' do
  446. before do
  447. get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  448. end
  449. it_behaves_like 'cachable response'
  450. it 'returns HTTP success' do
  451. expect(response).to have_http_status(200)
  452. end
  453. end
  454. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  455. describe endpoint do
  456. before do
  457. get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  458. end
  459. it_behaves_like 'non-cacheable response'
  460. it 'returns HTTP success' do
  461. expect(response).to have_http_status(200)
  462. end
  463. end
  464. end
  465. end
  466. context 'when providing a Signature from a non-allowed domain' do
  467. let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
  468. let(:dummy_signature) { 'dummy-signature' }
  469. describe '/actor' do
  470. before do
  471. get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  472. end
  473. it_behaves_like 'cachable response'
  474. it 'returns HTTP success' do
  475. expect(response).to have_http_status(200)
  476. end
  477. end
  478. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  479. describe endpoint do
  480. before do
  481. get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  482. end
  483. it_behaves_like 'non-cacheable error'
  484. end
  485. end
  486. end
  487. end
  488. context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do
  489. around do |example|
  490. ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do
  491. example.run
  492. end
  493. end
  494. context 'when anonymously accessed' do
  495. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  496. describe endpoint do
  497. before { get endpoint }
  498. it_behaves_like 'cachable response'
  499. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  500. end
  501. end
  502. TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
  503. describe endpoint do
  504. before { get endpoint }
  505. it_behaves_like 'non-cacheable response'
  506. end
  507. end
  508. (TestEndpoints::REQUIRE_TOKEN + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint|
  509. describe endpoint do
  510. before { get endpoint }
  511. it_behaves_like 'non-cacheable error'
  512. end
  513. end
  514. end
  515. context 'with an auth token' do
  516. let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
  517. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  518. describe endpoint do
  519. before do
  520. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  521. end
  522. it_behaves_like 'cachable response'
  523. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  524. end
  525. end
  526. TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
  527. describe endpoint do
  528. before do
  529. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  530. end
  531. it_behaves_like 'non-cacheable response'
  532. it 'has a Vary on Authorization' do
  533. expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('authorization')
  534. end
  535. end
  536. end
  537. (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint|
  538. describe endpoint do
  539. before do
  540. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  541. end
  542. it_behaves_like 'non-cacheable response'
  543. it 'returns HTTP success' do
  544. expect(response).to have_http_status(200)
  545. end
  546. end
  547. end
  548. end
  549. end
  550. end