1
0

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