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

    should "return 404 Not Found for an unsupported method on the root path" do
      post root_path
      assert_response 404

      put root_path
      assert_response 404

      patch root_path
      assert_response 404

      delete root_path
      assert_response 404
    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(page: 999_999_999), as: :json
        assert_response 410

        get posts_path(page: 999_999_999), as: :jpg
        assert_response 410

        get posts_path(page: 999_999_999), 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: ["1.2.3.4", "2.0.0.1/24", "2600::1/64"])

          ActionDispatch::Request.any_instance.stubs(:remote_ip).returns("1.2.3.4")
          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("2.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("2.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, session: { name: @user.name, password: "password" } }
          assert_redirected_to root_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")
        login_as(@user)
      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")
        Danbooru.config.stubs(:rate_limits_enabled?).returns(true)
        create(:rate_limit, action: "posts:update", key: user.cache_key, limited: 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, 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(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(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(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(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(expiry: "1"), as: :json

      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(expires_in: "5min"), as: :json

      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(only: "id,rating,score"), as: :json

      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
