cache_spec.rb 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  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 |http_success: false|
  111. it 'does not set cookies or set public cache control', :aggregate_failures do
  112. expect(response.cookies).to be_empty
  113. # expect(response.cache_control[:max_age]&.to_i).to be_positive
  114. expect(response.cache_control[:public]).to be_truthy
  115. expect(response.cache_control[:private]).to be_falsy
  116. expect(response.cache_control[:no_store]).to be_falsy
  117. expect(response.cache_control[:no_cache]).to be_falsy
  118. expect(response).to have_http_status(200) if http_success
  119. end
  120. end
  121. shared_examples 'non-cacheable response' do |http_success: false|
  122. it 'sets private cache control' do
  123. expect(response.cache_control[:private]).to be_truthy
  124. expect(response.cache_control[:no_store]).to be_truthy
  125. expect(response).to have_http_status(200) if http_success
  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_vary_headers).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_vary_headers).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_vary_headers).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. before do
  205. Setting.show_domain_blocks = show_domain_blocks
  206. get '/api/v1/instance/domain_blocks'
  207. end
  208. context 'when set to be publicly-available' do
  209. let(:show_domain_blocks) { 'all' }
  210. it_behaves_like 'cachable response'
  211. end
  212. context 'when allowed for local users only' do
  213. let(:show_domain_blocks) { 'users' }
  214. it_behaves_like 'non-cacheable error'
  215. end
  216. context 'when disabled' do
  217. let(:show_domain_blocks) { 'disabled' }
  218. it_behaves_like 'non-cacheable error'
  219. end
  220. end
  221. end
  222. context 'when logged in' do
  223. before do
  224. sign_in user, scope: :user
  225. # Unfortunately, devise's `sign_in` helper causes the `session` to be
  226. # loaded in the next request regardless of whether it's actually accessed
  227. # by the client code.
  228. #
  229. # So, we make an extra query to clear issue a session cookie instead.
  230. #
  231. # A less resource-intensive way to deal with that would be to generate the
  232. # session cookie manually, but this seems pretty involved.
  233. get '/'
  234. end
  235. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  236. describe endpoint do
  237. before { get endpoint }
  238. it_behaves_like 'cachable response'
  239. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  240. end
  241. end
  242. TestEndpoints::COOKIE_DEPENDENT_CACHABLE.each do |endpoint|
  243. describe endpoint do
  244. before { get endpoint }
  245. it_behaves_like 'non-cacheable response'
  246. it 'has a Vary on Cookie' do
  247. expect(response_vary_headers).to include('cookie')
  248. end
  249. end
  250. end
  251. TestEndpoints::REQUIRE_LOGIN.each do |endpoint|
  252. describe endpoint do
  253. before { get endpoint }
  254. it_behaves_like 'non-cacheable response', http_success: true
  255. end
  256. end
  257. TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
  258. describe endpoint do
  259. before { get endpoint }
  260. it_behaves_like 'non-cacheable error'
  261. end
  262. end
  263. end
  264. context 'with an auth token' do
  265. let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
  266. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  267. describe endpoint do
  268. before do
  269. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  270. end
  271. it_behaves_like 'cachable response'
  272. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  273. end
  274. end
  275. TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
  276. describe endpoint do
  277. before do
  278. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  279. end
  280. it_behaves_like 'non-cacheable response'
  281. it 'has a Vary on Authorization' do
  282. expect(response_vary_headers).to include('authorization')
  283. end
  284. end
  285. end
  286. (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN).each do |endpoint|
  287. describe endpoint do
  288. before do
  289. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  290. end
  291. it_behaves_like 'non-cacheable response', http_success: true
  292. end
  293. end
  294. describe '/api/v1/instance/domain_blocks' do
  295. before do
  296. Setting.show_domain_blocks = show_domain_blocks
  297. get '/api/v1/instance/domain_blocks', headers: { 'Authorization' => "Bearer #{token.token}" }
  298. end
  299. context 'when set to be publicly-available' do
  300. let(:show_domain_blocks) { 'all' }
  301. it_behaves_like 'cachable response'
  302. end
  303. context 'when allowed for local users only' do
  304. let(:show_domain_blocks) { 'users' }
  305. it_behaves_like 'non-cacheable response', http_success: true
  306. end
  307. context 'when disabled' do
  308. let(:show_domain_blocks) { 'disabled' }
  309. it_behaves_like 'non-cacheable error'
  310. end
  311. end
  312. end
  313. context 'with a Signature header' do
  314. let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
  315. let(:dummy_signature) { 'dummy-signature' }
  316. before do
  317. remote_actor.follow!(alice)
  318. end
  319. describe '/actor' do
  320. before do
  321. get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  322. end
  323. it_behaves_like 'cachable response', http_success: true
  324. end
  325. TestEndpoints::REQUIRE_SIGNATURE.each do |endpoint|
  326. describe endpoint do
  327. before do
  328. get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  329. end
  330. it_behaves_like 'non-cacheable response', http_success: true
  331. end
  332. end
  333. end
  334. context 'when enabling AUTHORIZED_FETCH mode' do
  335. around do |example|
  336. ClimateControl.modify AUTHORIZED_FETCH: 'true' do
  337. example.run
  338. end
  339. end
  340. context 'when not providing a Signature' do
  341. describe '/actor' do
  342. before do
  343. get '/actor', headers: { 'Accept' => 'application/activity+json' }
  344. end
  345. it_behaves_like 'cachable response', http_success: true
  346. end
  347. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  348. describe endpoint do
  349. before do
  350. get endpoint, headers: { 'Accept' => 'application/activity+json' }
  351. end
  352. it_behaves_like 'non-cacheable error'
  353. end
  354. end
  355. end
  356. context 'when providing a Signature' do
  357. let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
  358. let(:dummy_signature) { 'dummy-signature' }
  359. before do
  360. remote_actor.follow!(alice)
  361. end
  362. describe '/actor' do
  363. before do
  364. get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  365. end
  366. it_behaves_like 'cachable response', http_success: true
  367. end
  368. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  369. describe endpoint do
  370. before do
  371. get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  372. end
  373. it_behaves_like 'non-cacheable response', http_success: true
  374. end
  375. end
  376. end
  377. end
  378. context 'when enabling LIMITED_FEDERATION_MODE mode' do
  379. around do |example|
  380. ClimateControl.modify LIMITED_FEDERATION_MODE: 'true' do
  381. old_limited_federation_mode = Rails.configuration.x.limited_federation_mode
  382. Rails.configuration.x.limited_federation_mode = true
  383. example.run
  384. Rails.configuration.x.limited_federation_mode = old_limited_federation_mode
  385. end
  386. end
  387. context 'when not providing a Signature' do
  388. describe '/actor' do
  389. before do
  390. get '/actor', headers: { 'Accept' => 'application/activity+json' }
  391. end
  392. it_behaves_like 'cachable response', http_success: true
  393. end
  394. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  395. describe endpoint do
  396. before do
  397. get endpoint, headers: { 'Accept' => 'application/activity+json' }
  398. end
  399. it_behaves_like 'non-cacheable error'
  400. end
  401. end
  402. end
  403. context 'when providing a Signature from an allowed domain' do
  404. let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
  405. let(:dummy_signature) { 'dummy-signature' }
  406. before do
  407. DomainAllow.create!(domain: remote_actor.domain)
  408. remote_actor.follow!(alice)
  409. end
  410. describe '/actor' do
  411. before do
  412. get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  413. end
  414. it_behaves_like 'cachable response', http_success: true
  415. end
  416. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  417. describe endpoint do
  418. before do
  419. get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  420. end
  421. it_behaves_like 'non-cacheable response', http_success: true
  422. end
  423. end
  424. end
  425. context 'when providing a Signature from a non-allowed domain' do
  426. let(:remote_actor) { Fabricate(:account, domain: 'example.org', uri: 'https://example.org/remote', protocol: :activitypub) }
  427. let(:dummy_signature) { 'dummy-signature' }
  428. describe '/actor' do
  429. before do
  430. get '/actor', sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  431. end
  432. it_behaves_like 'cachable response', http_success: true
  433. end
  434. (TestEndpoints::REQUIRE_SIGNATURE + TestEndpoints::AuthorizedFetch::REQUIRE_SIGNATURE).each do |endpoint|
  435. describe endpoint do
  436. before do
  437. get endpoint, sign_with: remote_actor, headers: { 'Accept' => 'application/activity+json' }
  438. end
  439. it_behaves_like 'non-cacheable error'
  440. end
  441. end
  442. end
  443. end
  444. context 'when enabling DISALLOW_UNAUTHENTICATED_API_ACCESS' do
  445. around do |example|
  446. ClimateControl.modify DISALLOW_UNAUTHENTICATED_API_ACCESS: 'true' do
  447. example.run
  448. end
  449. end
  450. context 'when anonymously accessed' do
  451. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  452. describe endpoint do
  453. before { get endpoint }
  454. it_behaves_like 'cachable response'
  455. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  456. end
  457. end
  458. TestEndpoints::REQUIRE_LOGGED_OUT.each do |endpoint|
  459. describe endpoint do
  460. before { get endpoint }
  461. it_behaves_like 'non-cacheable response'
  462. end
  463. end
  464. (TestEndpoints::REQUIRE_TOKEN + TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint|
  465. describe endpoint do
  466. before { get endpoint }
  467. it_behaves_like 'non-cacheable error'
  468. end
  469. end
  470. end
  471. context 'with an auth token' do
  472. let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
  473. TestEndpoints::ALWAYS_CACHED.each do |endpoint|
  474. describe endpoint do
  475. before do
  476. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  477. end
  478. it_behaves_like 'cachable response'
  479. it_behaves_like 'language-dependent' if TestEndpoints::LANGUAGE_DEPENDENT.include?(endpoint)
  480. end
  481. end
  482. TestEndpoints::AUTHORIZATION_DEPENDENT_CACHABLE.each do |endpoint|
  483. describe endpoint do
  484. before do
  485. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  486. end
  487. it_behaves_like 'non-cacheable response'
  488. it 'has a Vary on Authorization' do
  489. expect(response_vary_headers).to include('authorization')
  490. end
  491. end
  492. end
  493. (TestEndpoints::REQUIRE_LOGGED_OUT + TestEndpoints::REQUIRE_TOKEN + TestEndpoints::DisabledAnonymousAPI::REQUIRE_TOKEN).each do |endpoint|
  494. describe endpoint do
  495. before do
  496. get endpoint, headers: { 'Authorization' => "Bearer #{token.token}" }
  497. end
  498. it_behaves_like 'non-cacheable response', http_success: true
  499. end
  500. end
  501. end
  502. end
  503. private
  504. def response_vary_headers
  505. response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }
  506. end
  507. end