# frozen_string_literal: true

#
# Copyright (C) 2016 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
require 'simple_oauth'

describe Lti::Security do
  describe '.signed_post_params' do
    let(:params) { {custom_a: 1, custom_b:2} }
    let(:consumer_key) { 'test' }
    let(:launch_url) { 'https://test.example/launch' }
    let(:consumer_secret) { 'shh'}

    context 'disable_lti_post_only' do
      it 'generates a correct signature' do

        signed_params = Lti::Security.signed_post_params(params, launch_url, consumer_key, consumer_secret, true)
        nonce = signed_params['oauth_nonce']
        timestamp = signed_params['oauth_timestamp']

        header = SimpleOAuth::Header.new(
          :post,
          launch_url,
          params,
          consumer_key: consumer_key,
          consumer_secret: consumer_secret,
          nonce: nonce,
          timestamp: timestamp
        )
        expect(header.valid?(signature: signed_params['oauth_signature'])).to eq true

      end

      it "doesn't copy query params" do
        signed_params = Lti::Security.signed_post_params(params, launch_url, consumer_key, consumer_secret, true)
        expect(signed_params.key?('test')).to eq false
      end

      it "logs the oauth base string" do
        allow(Lti::Logging).to receive(:lti_1_launch_generated)

        url = "http://test.example:3000/launch"
        signed_params = Lti::Security.signed_post_params(params, url, consumer_key, consumer_secret, true)

        expect(Lti::Logging).to have_received(:lti_1_launch_generated).with(%W{
          POST&http%3A%2F%2Ftest.example%3A3000%2Flaunch&custom_a%3D1%26custom_b%3D2%26
          oauth_consumer_key%3Dtest%26
          oauth_nonce%3D#{signed_params['oauth_nonce']}%26
          oauth_signature_method%3DHMAC-SHA1%26
          oauth_timestamp%3D#{signed_params['oauth_timestamp']}%26
          oauth_version%3D1.0}.join)
      end
    end

    context '#decoded_lti_assignment_id' do
      it 'returns nil if secure params are invalid' do
        expect(Lti::Security.decoded_lti_assignment_id('banana')).to be_nil
      end

      it 'returns the lti assignment id if secure params are valid' do
        assignment_id = 12
        body = {lti_assignment_id: assignment_id}
        secure_params = Canvas::Security.create_jwt(body).to_s
        expect(Lti::Security.decoded_lti_assignment_id(secure_params)).to eq assignment_id
      end
    end

    context '.check_and_store_nonce' do
      it 'rejects a used nonce' do
        enable_cache do
          cache_key = 'abcdefghijklmnopqrstuvwxyz'
          timestamp = 1.minute.ago
          expiration = 5.minutes
          params = [cache_key, timestamp, expiration]
          expect(Lti::Security.check_and_store_nonce(*params)).to be true
          expect(Lti::Security.check_and_store_nonce(*params)).to be false
        end
      end

      it 'rejects a nonce if the timestamp exceeds the expiration' do
        cache_key = 'abcdefghijklmnopqrstuvwxyz'
        expiration = 5.minutes
        timestamp = (expiration + 1.minute).ago.to_i
        expect(Lti::Security.check_and_store_nonce(cache_key, timestamp, expiration)).to be false
      end

      it 'rejects a nonce more than 1 minute in the future' do
        cache_key = 'abcdefghijklmnopqrstuvwxyz'
        expiration = 5.minutes
        timestamp = 61.seconds.from_now
        expect(Lti::Security.check_and_store_nonce(cache_key, timestamp, expiration)).to be false
      end

      it 'accepts a nonce less than 1 minute in the future' do
        cache_key = 'abcdefghijklmnopqrstuvwxyz'
        expiration = 5.minutes
        timestamp = 59.seconds.from_now
        expect(Lti::Security.check_and_store_nonce(cache_key, timestamp, expiration)).to be true
      end

    end

    it "generates a correct signature" do
      signed_params = Lti::Security.signed_post_params(params, launch_url, consumer_key, consumer_secret)

      nonce = signed_params['oauth_nonce']
      timestamp = signed_params['oauth_timestamp']

      header = SimpleOAuth::Header.new(
        :post,
        launch_url,
        params,
        consumer_key: consumer_key,
        consumer_secret: consumer_secret,
        nonce: nonce,
        timestamp: timestamp
      )
      expect(header.valid?(signature: signed_params['oauth_signature'])).to eq true
    end

    it "handles whitespace in URLs" do
      url_with_whitespace = "http://www.test.com "
      signed_params = Lti::Security.signed_post_params(params, url_with_whitespace, consumer_key, consumer_secret)

      nonce = signed_params['oauth_nonce']
      timestamp = signed_params['oauth_timestamp']

      header = SimpleOAuth::Header.new(
        :post,
        url_with_whitespace.strip,
        params,
        consumer_key: consumer_key,
        consumer_secret: consumer_secret,
        nonce: nonce,
        timestamp: timestamp
      )
      expect(header.valid?(signature: signed_params['oauth_signature'])).to eq true
    end

    it "generates the signature for urls with query params in an incorrect way that we are aware of and saddened by" do
      # in this set of conditions the old code moves the query params to the body, uses the url minus the query params
      # for the base string, and then launches to the full url..... :-(
      url = launch_url + '?test=foo'
      params_copy = params.dup
      signed_params = Lti::Security.signed_post_params(params_copy, url, consumer_key, consumer_secret)

      nonce = signed_params['oauth_nonce']
      timestamp = signed_params['oauth_timestamp']

      header = SimpleOAuth::Header.new(
        :post,
        launch_url, # Note that we are using a different url to generate a signature than before :-(
        params.merge(test: 'foo'),
        consumer_key: consumer_key,
        consumer_secret: consumer_secret,
        nonce: nonce,
        timestamp: timestamp
      )
      expect(header.valid?(signature: signed_params['oauth_signature'])).to eq true
    end

    it "generates the signature correctly for a non standard port" do
      url = "http://test.example:3000/launch"

      signed_params = Lti::Security.signed_post_params(params, url, consumer_key, consumer_secret)
      nonce = signed_params['oauth_nonce']
      timestamp = signed_params['oauth_timestamp']

      header = SimpleOAuth::Header.new(
        :post,
        url,
        params,
        consumer_key: consumer_key,
        consumer_secret: consumer_secret,
        nonce: nonce,
        timestamp: timestamp
      )
      expect(header.valid?(signature: signed_params['oauth_signature'])).to eq true

    end

    it "logs the oauth base string" do
      allow(Lti::Logging).to receive(:lti_1_launch_generated)

      url = "http://test.example:3000/launch"
      signed_params = Lti::Security.signed_post_params(params, url, consumer_key, consumer_secret)
      nonce = signed_params['oauth_nonce']
      timestamp = signed_params['oauth_timestamp']

      expect(Lti::Logging).to have_received(:lti_1_launch_generated).with(%W{
        POST&http%3A%2F%2Ftest.example%3A3000%2Flaunch&custom_a%3D1%26custom_b%3D2%26
        oauth_consumer_key%3Dtest%26oauth_consumer_key%3Dtest%26
        oauth_nonce%3D#{nonce}%26oauth_nonce%3D#{nonce}%26
        oauth_signature_method%3DHMAC-SHA1%26oauth_signature_method%3DHMAC-SHA1%26
        oauth_timestamp%3D#{timestamp}%26oauth_timestamp%3D#{timestamp}%26
        oauth_version%3D1.0%26oauth_version%3D1.0}.join)
    end
  end
end
