require "test_helper" class ApplicationControllerTest < ActionDispatch::IntegrationTest context "The application controller" do should "return 406 Not Acceptable for a bad file extension" do get posts_path, params: { format: :jpg } assert_response 406 get posts_path, params: { format: :blah } assert_response 406 end should "return 400 Bad Request for a GET request with a body" do get root_path, headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, env: { RAW_POST_DATA: "tags=touhou" } assert_response 400 assert_equal("ApplicationController::RequestBodyNotAllowedError", response.parsed_body["error"]) assert_equal("Request body not allowed for GET request", response.parsed_body["message"]) end should "return 200 OK for a POST request overridden to be a GET request" do post root_path, headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "X-Http-Method-Override": "GET" }, env: { RAW_POST_DATA: "tags=touhou" } assert_response 200 end context "on a RecordNotFound error" do should "return 404 Not Found even with a bad file extension" do get post_path("bad.json") assert_response 404 get post_path("bad.jpg") assert_response 404 get post_path("bad.blah") assert_response 404 end end context "on a PaginationError" do should "return 410 Gone even with a bad file extension" do get posts_path, params: { page: 999999999 }, as: :json assert_response 410 get posts_path, params: { page: 999999999 }, as: :jpg assert_response 410 get posts_path, params: { page: 999999999 }, as: :blah assert_response 410 end end context "on an unexpected error" do setup do User.stubs(:find).raises(NoMethodError.new("pwned")) @user = create(:user) end should "not return the error message in the HTML response" do get user_path(@user) assert_response 500 assert_match(/NoMethodError/, response.body.to_s) assert_no_match(/pwned/, response.body.to_s) end should "not return the error message in the JSON response" do get user_path(@user, format: :json) assert_response 500 assert_match(/NoMethodError/, response.body.to_s) assert_no_match(/pwned/, response.body.to_s) end should "not return the error message in the XML response" do get user_path(@user, format: :xml) assert_response 500 assert_match(/NoMethodError/, response.body.to_s) assert_no_match(/pwned/, response.body.to_s) end should "not return the error message in the JS response" do get user_path(@user, format: :js) assert_response 500 assert_match(/NoMethodError/, response.body.to_s) assert_no_match(/pwned/, response.body.to_s) end end context "when a user has an invalid username" do should "redirect to the name change page" do @user = create(:user) @user.update_columns(name: "foo__bar") get_auth posts_path, @user assert_redirected_to change_name_user_path(@user) end end context "on api authentication" do setup do @user = create(:user, password: "password") @api_key = create(:api_key, user: @user) ActionController::Base.allow_forgery_protection = true end teardown do ActionController::Base.allow_forgery_protection = false end context "using http basic auth" do should "succeed for api key matches" do basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key.key}")}" get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string } assert_response :success assert_equal(1, @api_key.reload.uses) assert_not_nil(@api_key.reload.last_used_at) end should "succeed when the user has multiple api keys" do @api_key2 = create(:api_key, user: @user) basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key2.key}")}" get edit_user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string } assert_response :success assert_equal(1, @api_key2.reload.uses) assert_not_nil(@api_key2.reload.last_used_at) end should "fail for api key mismatches" do basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:badpassword")}" get profile_path, as: :json, headers: { HTTP_AUTHORIZATION: basic_auth_string } assert_response 401 end should "fail for a deleted user" do @user.update!(is_deleted: true) basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key.key}")}" get profile_path, as: :json, headers: { HTTP_AUTHORIZATION: basic_auth_string } assert_response 401 end should "succeed for non-GET requests without a CSRF token" do assert_changes -> { @user.reload.enable_safe_mode }, from: false, to: true do basic_auth_string = "Basic #{::Base64.encode64("#{@user.name}:#{@api_key.key}")}" put user_path(@user), headers: { HTTP_AUTHORIZATION: basic_auth_string }, params: { user: { enable_safe_mode: "true" } }, as: :json assert_response :success end end end context "using the api_key parameter" do should "succeed for api key matches" do get edit_user_path(@user), params: { login: @user.name, api_key: @api_key.key } assert_response :success assert_equal(1, @api_key.reload.uses) assert_not_nil(@api_key.reload.last_used_at) end should "succeed when the user has multiple api keys" do @api_key2 = create(:api_key, user: @user) get edit_user_path(@user), params: { login: @user.name, api_key: @api_key2.key } assert_response :success assert_equal(1, @api_key2.reload.uses) assert_not_nil(@api_key2.reload.last_used_at) end should "fail for api key mismatches" do get profile_path(login: @user.name), as: :json assert_response 401 get profile_path(api_key: @api_key.key), as: :json assert_response 401 get profile_path(login: @user.name, api_key: "bad"), as: :json assert_response 401 end should "fail for a blank API key" do get profile_path(login: ""), as: :json assert_response 401 get profile_path(api_key: ""), as: :json assert_response 401 end should "fail for a deleted user" do @user.update!(is_deleted: true) get edit_user_path(@user), params: { login: @user.name, api_key: @api_key.key } assert_response 401 end should "succeed for non-GET requests without a CSRF token" do assert_changes -> { @user.reload.enable_safe_mode }, from: false, to: true do put user_path(@user, login: @user.name, api_key: @api_key.key), params: { user: { enable_safe_mode: "true" }}, as: :json assert_response :success end end end context "for an API key with restrictions" do should "restrict requests to the permitted IP addresses" do @api_key = create(:api_key, permitted_ip_addresses: ["192.168.0.1", "10.0.0.1/24", "2600::1/64"]) ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("192.168.0.1") get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key } assert_response :success ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("10.0.0.42") get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key } assert_response :success ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("2600::1234:0:0:1") get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key } assert_response :success ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("127.0.0.2") get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key } assert_response 403 ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("10.0.1.0") get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key } assert_response 403 ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("2600:dead:beef::1") get posts_path, params: { login: @api_key.user.name, api_key: @api_key.key } assert_response 403 assert_equal(6, @api_key.reload.uses) assert_equal("2600:dead:beef::1", @api_key.reload.last_ip_address.to_s) assert_not_nil(@api_key.reload.last_used_at) end should "restrict requests to the permitted endpoints" do @post = create(:post) @api_key = create(:api_key, permissions: ["posts:index", "posts:show"]) get posts_path(login: @api_key.user.name, api_key: @api_key.key) assert_response :success get post_path(@post, login: @api_key.user.name, api_key: @api_key.key) assert_response :success get tags_path(login: @api_key.user.name, api_key: @api_key.key) assert_response 403 put post_path(@post, login: @api_key.user.name, api_key: @api_key.key), params: { post: { rating: "s" }} assert_response 403 assert_equal(4, @api_key.reload.uses) assert_equal("127.0.0.1", @api_key.reload.last_ip_address.to_s) assert_not_nil(@api_key.reload.last_used_at) end end context "with cookie-based authentication" do should "not allow non-GET requests without a CSRF token" do # get the csrf token from the login page so we can login get new_session_path assert_response :success token = css_select("form input[name=authenticity_token]").first["value"] # login post session_path, params: { authenticity_token: token, name: @user.name, password: "password" } assert_redirected_to posts_path # try to submit a form with cookies but without the csrf token put user_path(@user), headers: { HTTP_COOKIE: headers["Set-Cookie"] }, params: { user: { enable_safe_mode: "true" } } assert_response 403 assert_equal("Error: Can't verify CSRF token authenticity.", css_select("p").first.content) assert_equal(false, @user.reload.enable_safe_mode) end end end context "on session cookie authentication" do setup do @user = create(:user, password: "password") post session_path, params: { name: @user.name, password: "password" } end should "succeed" do get profile_path assert_response :success end should "fail for a deleted user" do @user.update!(is_deleted: true) get profile_path assert_redirected_to login_path(url: "/profile") assert_nil(session[:user_id]) assert_equal(true, @user.user_events.exists?(category: :logout)) end end context "accessing an unauthorized page" do should "render the access denied page" do get news_updates_path assert_response 403 assert_select "h1", /Access Denied/ end should "render a json response for json requests" do get news_updates_path(format: :json) assert_response 403 assert_equal "application/json", response.media_type assert_equal "Access denied", response.parsed_body["message"] end end context "when the api limit is exceeded" do should "fail with a 429 error" do user = create(:user) post = create(:post, rating: "s") RateLimit.any_instance.stubs(:limited?).returns(true) put_auth post_path(post), user, params: { post: { rating: "e" } } assert_response 429 assert_equal("s", post.reload.rating) end end end context "all index methods" do should "support searching by the id attribute" do tags = create_list(:tag, 2, post_count: 42) get tags_path(format: :json), params: { search: { id: tags.first.id } } assert_response :success assert_equal(1, response.parsed_body.size) assert_equal(tags.first.id, response.parsed_body.first.fetch("id")) end should "support ordering by search[order]=custom" do tags = create_list(:tag, 2, post_count: 42) get tags_path, params: { search: { id: "#{tags[0].id},#{tags[1].id}", order: "custom" } }, as: :json assert_response :success assert_equal(tags.pluck(:id), response.parsed_body.pluck("id")) end should "return nothing if the search[order]=custom param isn't accompanied by search[id]" do tags = create_list(:tag, 2, post_count: 42) get tags_path, params: { search: { order: "custom" } }, as: :json assert_response :success assert_equal(0, response.parsed_body.size) end should "return nothing if the search[order]=custom param isn't accompanied by a valid search[id]" do tags = create_list(:tag, 2, post_count: 42) get tags_path, params: { search: { id: ">1", order: "custom" } }, as: :json assert_response :success assert_equal(0, response.parsed_body.size) end should "work if the search[order]=custom param is used with a single id" do tags = create_list(:tag, 2, post_count: 42) get tags_path, params: { search: { id: tags[0].id, order: "custom" } }, as: :json assert_response :success assert_equal([tags[0].id], response.parsed_body.pluck("id")) end should "remove blank `search` params from the URL" do get tags_path(search: { name: "touhou", blah: "" }), as: :json assert_redirected_to tags_path(search: { name: "touhou" }) end should "ignore invalid `search` params" do get tags_path(search: "foo"), as: :json assert_response :success get tags_path("search[]": "foo"), as: :json assert_response :success end should "support the expiry parameter" do get posts_path, as: :json, params: { expiry: "1" } assert_response :success assert_equal("max-age=#{1.day}, private", response.headers["Cache-Control"]) end should "support the expires_in parameter" do get posts_path, as: :json, params: { expires_in: "5min" } assert_response :success assert_equal("max-age=#{5.minutes}, private", response.headers["Cache-Control"]) end should "support the only parameter" do create(:post) get posts_path, as: :json, params: { only: "id,rating,score" } assert_response :success assert_equal(%w[id rating score].sort, response.parsed_body.first.keys.sort) end should "return the correct root element name for empty xml responses" do get tags_path, as: :xml assert_response :success assert_equal("tags", response.parsed_body.root.name) assert_equal(0, response.parsed_body.root.children.size) end end end