require "spec_helper"

describe Api::OpenidConnect::AuthorizationsController, type: :controller do
  let!(:client) { FactoryGirl.create(:o_auth_application) }
  let!(:client_with_xss) { FactoryGirl.create(:o_auth_application_with_xss) }
  let!(:client_with_multiple_redirects) { FactoryGirl.create(:o_auth_application_with_multiple_redirects) }
  let!(:auth_with_read) { FactoryGirl.create(:auth_with_read) }

  before do
    sign_in :user, alice
  end

  describe "#new" do
    context "when not yet authorized" do
      context "when valid parameters are passed" do
        render_views
        context "as GET request" do
          it "should return a form page" do
            get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
                scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
            expect(response.body).to match("Diaspora Test Client")
          end
        end

        context "using claims" do
          it "should return a form page" do
            get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
                scope: "openid", claims: "{\"userinfo\": {\"name\": {\"essential\": true}}}",
                nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
            expect(response.body).to match("Diaspora Test Client")
          end
        end

        context "as a request object" do
          it "should return a form page" do
            header = JWT.encoded_header("none")
            payload_hash = {client_id: client.client_id, redirect_uri: "http://localhost:3000/",
                            response_type: "id_token", scope: "openid", nonce: "hello", state: "hello",
                            claims: {userinfo: {name: {essential: true}}}}
            payload = JWT.encoded_payload(JSON.parse(payload_hash.to_json))
            request_object = header + "." + payload + "."
            get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
                scope: "openid", nonce: "hello", state: "hello", request: request_object
            expect(response.body).to match("Diaspora Test Client")
          end
        end

        context "as a request object with no claims" do
          it "should return a form page" do
            header = JWT.encoded_header("none")
            payload_hash = {client_id: client.client_id, redirect_uri: "http://localhost:3000/",
                             response_type: "id_token", scope: "openid", nonce: "hello", state: "hello"}
            payload = JWT.encoded_payload(JSON.parse(payload_hash.to_json))
            request_object = header + "." + payload + "."
            get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
                scope: "openid", nonce: "hello", state: "hello", request: request_object
            expect(response.body).to match("Diaspora Test Client")
          end
        end

        context "as POST request" do
          it "should return a form page" do
            post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
                 scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
            expect(response.body).to match("Diaspora Test Client")
          end
        end
      end

      context "when client id is missing" do
        it "should return an bad request error" do
          post :new, redirect_uri: "http://localhost:3000/", response_type: "id_token",
               scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
          expect(response.body).to include("The request was malformed")
        end
      end

      context "when redirect uri is missing" do
        context "when only one redirect URL is pre-registered" do
          it "should return a form page" do
            # Note this intentionally behavior diverts from OIDC spec http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
            # When client has only one redirect uri registered, only that redirect uri can be used. Hence,
            # we should implicitly assume the client wants to use that registered URI.
            # See https://github.com/nov/rack-oauth2/blob/master/lib/rack/oauth2/server/authorize.rb#L63
            post :new, client_id: client.client_id, response_type: "id_token",
                 scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
            expect(response.body).to match("Diaspora Test Client")
          end
        end
      end

      context "when multiple redirect URLs are pre-registered" do
        it "should return an invalid request error" do
          post :new, client_id: client_with_multiple_redirects.client_id, response_type: "id_token",
               scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
          expect(response.body).to include("The request was malformed")
        end
      end

      context "when redirect URI does not match pre-registered URIs" do
        it "should return an invalid request error", focus: true do
          post :new, client_id: client.client_id, redirect_uri: "http://localhost:2000/",
               response_type: "id_token", scope: "openid", nonce: SecureRandom.hex(16)
          expect(response.body).to include("Invalid client id or redirect uri")
        end
      end

      context "when an unsupported scope is passed in" do
        it "should return an invalid scope error" do
          post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
               scope: "random", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
          expect(response.body).to match("error=invalid_scope")
        end
      end

      context "when nonce is missing" do
        it "should return an invalid request error" do
          post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
               response_type: "id_token", scope: "openid", state: SecureRandom.hex(16)
          expect(response.location).to match("error=invalid_request")
        end
      end

      context "when prompt is none" do
        it "should return an interaction required error" do
          post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
               response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none"
          expect(response.body).to include("User must already be authorized when `prompt` is `none`")
        end
      end

      context "when prompt is none and user not signed in" do
        before do
          sign_out :user
        end

        it "should return an interaction required error" do
          post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
               response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none"
          expect(response.body).to include("User must already be logged in when `prompt` is `none`")
        end
      end

      context "when prompt is none and consent" do
        it "should return an interaction required error" do
          post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
               response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none consent"
          expect(response.location).to match("error=invalid_request")
        end
      end

      context "when prompt is select_account" do
        it "should return an account_selection_required error" do
          post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
               response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "select_account"
          expect(response.location).to match("error=account_selection_required")
          expect(response.location).to match("state=1234")
        end
      end

      context "when prompt is none and client ID is invalid" do
        it "should return an account_selection_required error" do
          post :new, client_id: "random", redirect_uri: "http://localhost:3000/",
               response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none"
          expect(response.body).to include("Invalid client id or redirect uri")
        end
      end

      context "when prompt is none and redirect URI does not match pre-registered URIs" do
        it "should return an account_selection_required error" do
          post :new, client_id: client.client_id, redirect_uri: "http://randomuri:3000/",
               response_type: "id_token", scope: "openid", state: 1234, display: "page", prompt: "none"
          expect(response.body).to include("Invalid client id or redirect uri")
        end
      end

      context "when XSS script is passed as name" do
        it "should escape html" do
          post :new, client_id: client_with_xss.client_id, redirect_uri: "http://localhost:3000/",
               response_type: "id_token", scope: "openid", nonce: SecureRandom.hex(16), state: SecureRandom.hex(16)
          expect(response.body).to_not include("<script>alert(0);</script>")
        end
      end
    end

    context "when already authorized" do
      let!(:auth) {
        Api::OpenidConnect::Authorization.find_or_create_by(o_auth_application: client, user: alice,
                                                            redirect_uri: "http://localhost:3000/", scopes: ["openid"])
      }

      context "when valid parameters are passed" do
        before do
          get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
              scope: "openid", nonce: 413_093_098_3, state: 413_093_098_3
        end

        it "should return the id token in a fragment" do
          expect(response.location).to have_content("id_token=")
          encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
          decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
                                                                        Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
          expect(decoded_token.nonce).to eq("4130930983")
          expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
        end

        it "should return the passed in state" do
          expect(response.location).to have_content("state=4130930983")
        end
      end

      context "when prompt is none" do
        it "should return the id token in a fragment" do
          post :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
               response_type: "id_token", scope: "openid", nonce: 413_093_098_3, state: 413_093_098_3,
               display: "page", prompt: "none"
          expect(response.location).to have_content("id_token=")
          encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
          decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
                                                                        Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
          expect(decoded_token.nonce).to eq("4130930983")
          expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
        end
      end

      context "when prompt contains consent" do
        it "should return a consent form page" do
          get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/",
              response_type: "id_token", scope: "openid", nonce: 413_093_098_3, state: 413_093_098_3,
              display: "page", prompt: "consent"
          expect(response.body).to match("Diaspora Test Client")
        end
      end
    end
  end

  describe "#create" do
    context "when id_token token" do
      before do
        get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token token",
            scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
      end

      context "when authorization is approved" do
        before do
          post :create, approve: "true"
        end

        it "should return the id token in a fragment" do
          encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
          decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
                                                                        Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
          expect(decoded_token.nonce).to eq("4180930983")
          expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
        end

        it "should return a valid access token in a fragment" do
          encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
          decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
                                                                        Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
          access_token = response.location[/(?<=access_token=)[^&]+/]
          access_token_check_num = UrlSafeBase64.encode64(OpenSSL::Digest::SHA256.digest(access_token)[0, 128 / 8])
          expect(decoded_token.at_hash).to eq(access_token_check_num)
        end
      end
    end

    context "when id_token" do
      before do
        get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
            scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
      end

      context "when authorization is approved" do
        before do
          post :create, approve: "true"
        end

        it "should return the id token in a fragment" do
          expect(response.location).to have_content("id_token=")
          encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
          decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token,
                                                                        Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY
          expect(decoded_token.nonce).to eq("4180930983")
          expect(decoded_token.exp).to be > Time.zone.now.utc.to_i
        end

        it "should return the passed in state" do
          expect(response.location).to have_content("state=4180930983")
        end
      end

      context "when authorization is denied" do
        before do
          post :create, approve: "false"
        end

        it "should return an error in the fragment" do
          expect(response.location).to have_content("error=")
        end

        it "should NOT contain a id token in the fragment" do
          expect(response.location).to_not have_content("id_token=")
        end
      end
    end

    context "when code" do
      before do
        get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "code",
            scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
      end

      context "when authorization is approved" do
        before do
          post :create, approve: "true"
        end

        it "should return the code" do
          expect(response.location).to have_content("code")
        end

        it "should return the passed in state" do
          expect(response.location).to have_content("state=4180930983")
        end
      end

      context "when authorization is denied" do
        before do
          post :create, approve: "false"
        end

        it "should return an error" do
          expect(response.location).to have_content("error")
        end

        it "should NOT contain code" do
          expect(response.location).to_not have_content("code")
        end
      end
    end
  end

  describe "#destroy" do
    context "with existent authorization" do
      before do
        delete :destroy, id: auth_with_read.id
      end

      it "removes the authorization" do
        expect(Api::OpenidConnect::Authorization.find_by(id: auth_with_read.id)).to be_nil
      end
    end

    context "with non-existent authorization" do
      it "raises an error" do
        delete :destroy, id: 123_456_789
        expect(response).to redirect_to(api_openid_connect_user_applications_url)
        expect(flash[:error]).to eq("The attempt to revoke the authorization with ID 123456789 has failed")
      end
    end
  end
end