RSpec.describe Mdm::Workspace, type: :model do
  subject(:workspace) do
    FactoryGirl.build(:mdm_workspace)
  end

  let(:default) do
    'default'
  end

  it_should_behave_like 'Metasploit::Concern.run'

  context 'factory' do
    it 'should be valid' do
      workspace = FactoryGirl.build(:mdm_workspace)
      expect(workspace).to be_valid
    end
  end

  context '#destroy' do
    it 'should successfully destroy the object and dependent objects' do
      workspace = FactoryGirl.create(:mdm_workspace)
      listener = FactoryGirl.create(:mdm_listener, :workspace => workspace)
      task = FactoryGirl.create(:mdm_task, :workspace => workspace)

      expect {
        workspace.destroy
      }.to_not raise_error
      expect {
        workspace.reload
      }.to raise_error(ActiveRecord::RecordNotFound)
      expect {
        listener.reload
      }.to raise_error(ActiveRecord::RecordNotFound)
      expect {
        task.reload
      }.to raise_error(ActiveRecord::RecordNotFound)
    end
  end

  context 'associations' do
    it { is_expected.to have_many(:clients).class_name('Mdm::Client').through(:hosts) }
    it { is_expected.to have_many(:creds).class_name('Mdm::Cred').through(:services) }
    it { is_expected.to have_many(:events).class_name('Mdm::Event') }
    it { is_expected.to have_many(:exploited_hosts).class_name('Mdm::ExploitedHost').through(:hosts) }
    it { is_expected.to have_many(:hosts).class_name('Mdm::Host') }
    it { is_expected.to have_many(:listeners).class_name('Mdm::Listener').dependent(:destroy) }
    it { is_expected.to have_many(:loots).class_name('Mdm::Loot').through(:hosts) }
    it { is_expected.to have_many(:notes).class_name('Mdm::Note') }
    it { is_expected.to belong_to(:owner).class_name('Mdm::User').with_foreign_key('owner_id') }
    it { is_expected.to have_many(:services).class_name('Mdm::Service').through(:hosts).with_foreign_key('service_id') }
    it { is_expected.to have_many(:sessions).class_name('Mdm::Session').through(:hosts) }
    it { is_expected.to have_many(:tasks).class_name('Mdm::Task').dependent(:destroy).order('created_at DESC') }
    it { is_expected.to have_and_belong_to_many(:users).class_name('Mdm::User') }
    it { is_expected.to have_many(:vulns).class_name('Mdm::Vuln').through(:hosts) }
  end

  context 'callbacks' do
    context 'before_save' do
      context '#normalize' do
        it 'should be called' do
          expect(workspace).to receive(:normalize)
          workspace.run_callbacks(:save)
        end
      end
    end
  end

  context 'columns' do
    it { is_expected.to have_db_column(:boundary).of_type(:string).with_options(:limit => 4 * (2 ** 10)) }
    it { is_expected.to have_db_column(:description).of_type(:string).with_options(:limit => 4 * (2 ** 10)) }
    it { is_expected.to have_db_column(:limit_to_network).of_type(:boolean).with_options(:default => false, :null => false) }
    it { is_expected.to have_db_column(:name).of_type(:string) }
    it { is_expected.to have_db_column(:owner_id).of_type(:integer) }

    context 'timestamps' do
      it { is_expected.to have_db_column(:created_at).of_type(:datetime).with_options(:null => false) }
      it { is_expected.to have_db_column(:updated_at).of_type(:datetime).with_options(:null => false) }
    end
  end

  context 'CONSTANTS' do
    it 'should define the DEFAULT name' do
      expect(described_class::DEFAULT).to eq(default)
    end
  end

  context 'validations' do
    context 'boundary' do
      let(:boundary) do
        nil
      end

      let(:error) do
        'must be a valid IP range'
      end

      before(:each) do
        workspace.boundary = boundary
        workspace.valid?
      end

      it 'should validate using #valid_ip_or_range?' do
        expect(workspace).to receive(:valid_ip_or_range?).with(boundary).and_return(false)

        workspace.valid?
      end

      context 'with valid IP' do
        let(:boundary) do
          '192.168.0.1'
        end

        it 'should not record an error' do
          expect(workspace.errors[:boundary]).not_to include(error)
        end
      end

      context 'with valid range' do
        let(:boundary) do
          '192.168.0.1/24'
        end

        it 'should not record an error' do
          expect(workspace.errors[:boundary]).not_to include(error)
        end
      end

      context 'with invalid IP or range' do
        let(:boundary) do
          '192.168'
        end

        it 'should record error that boundary must be a valid IP range', :pending => 'https://www.pivotaltracker.com/story/show/43171927' do
          expect(workspace).not_to be_valid
          expect(workkspace.errors[:boundary]).to include(error)
        end
      end
    end

    context 'description' do
      it { is_expected.to ensure_length_of(:description).is_at_most(4 * (2 ** 10)) }
    end

    context 'name' do
      it { is_expected.to ensure_length_of(:name).is_at_most(2**8 - 1) }
      it { is_expected.to validate_presence_of :name }
      it { is_expected.to validate_uniqueness_of :name }
    end
  end

  context 'methods' do
    let(:hosts) do
      FactoryGirl.create_list(:mdm_host, 2, :workspace => workspace)
    end

    let(:other_hosts) do
      FactoryGirl.create_list(:mdm_host, 2, :workspace => other_workspace)
    end

    let(:other_services) do
      other_hosts.collect do |host|
        FactoryGirl.create(:mdm_service, :host => host)
      end
    end

    let(:other_web_sites) do
      other_services.collect { |service|
        FactoryGirl.create(:mdm_web_site, :service => service)
      }
    end

    let(:other_workspace) do
      FactoryGirl.create(:mdm_workspace)
    end

    let(:services) do
      hosts.collect do |host|
        FactoryGirl.create(:mdm_service, :host => host)
      end
    end

    let(:web_sites) do
      services.collect { |service|
        FactoryGirl.create(:mdm_web_site, :service => service)
      }
    end

    context '#creds' do
      #
      # Let!s (let + before(:each))
      #

      let!(:creds) do
        services.collect do |service|
          FactoryGirl.create(:mdm_cred, :service => service)
        end
      end

      let!(:other_creds) do
        other_services.collect do |service|
          FactoryGirl.create(:mdm_cred, :service => service)
        end
      end

      it 'should be an ActiveRecord::Relation', :pending => 'https://www.pivotaltracker.com/story/show/43219917' do
        should be_a ActiveRecord::Relation
      end

      it 'should include services' do
        # to_a to make query return instances
        found_creds = workspace.creds.to_a

        expect(found_creds.length).to be > 0

        expect(
            found_creds.none? { |found_cred|
              found_cred.service.nil?
            }
        ).to eq(true)
      end

      it 'should return only Mdm::Creds from hosts in workspace' do
        found_creds = workspace.creds

        expect(found_creds.length).to eq(creds.length)

        expect(
            found_creds.all? { |cred|
              cred.service.host.workspace == workspace
            }
        ).to eq(true)
      end
    end

    context 'default' do
      context 'with default workspace' do
        before(:each) do
          FactoryGirl.create(
              :mdm_workspace,
              :name => default
          )
        end

        it 'should not create workspace' do
          workspace = nil

          expect {
            workspace = described_class.default
          }.to change(Mdm::Workspace, :count).by(0)

          expect(workspace).to be_default
        end
      end

      context 'without default workspace' do
        it 'should create workspace' do
          workspace = nil

          expect {
            workspace = described_class.default
          }.to change(Mdm::Workspace, :count).by(1)

          expect(workspace).to be_default
        end
      end
    end

    context '#default?' do
      subject do
        workspace.default?
      end

      context 'with DEFAULT name' do
        before(:each) do
          workspace.name = default
        end

        it { is_expected.to eq(true) }
      end

      context 'without DEFAULT name' do
        it { is_expected.to eq(false) }
      end
    end

    context '#each_cred' do
      it 'should pass each of the #creds to the block' do
        creds = FactoryGirl.create_list(:mdm_cred, 2)
        allow(workspace).to receive(:creds).and_return(creds)

        expect { |block|
          workspace.each_cred(&block)
        }.to yield_successive_args(*creds)
      end
    end

    context '#each_host_tag' do
      it 'should pass each of the #host_tags to the block' do
        tags = FactoryGirl.create_list(:mdm_tag, 2)
        expect(workspace).to receive(:host_tags).and_return(tags)

        expect { |block|
          workspace.each_host_tag(&block)
        }.to yield_successive_args(*tags)
      end
    end

    context '#host_tags' do
      subject(:host_tags) do
        workspace.host_tags
      end

      #
      # lets
      #

      let(:other_tags) do
        FactoryGirl.create_list(
            :mdm_tag,
            2
        )
      end

      let(:tags) do
        FactoryGirl.create_list(
            :mdm_tag,
            2
        )
      end

      #
      # Let!s (let + before(:each))
      #

      let!(:first_host_tags) do
        host_tags = []

        hosts.zip(tags) do |host, tag|
          host_tag = FactoryGirl.create(:mdm_host_tag, :host => host, :tag => tag)

          host_tags << host_tag
        end

        host_tags
      end

      let!(:second_host_tags) do
        host_tags = []

        other_hosts.zip(other_tags) do |host, tag|
          host_tag = FactoryGirl.create(:mdm_host_tag, :host => host, :tag => tag)

          host_tags << host_tag
        end

        host_tags
      end

      it 'should return an ActiveRecord::Relation', :pending => 'https://www.pivotaltracker.com/story/show/43219917' do
        should be_a ActiveRecord::Relation
      end

      it 'should return only Mdm::Tags from hosts in the workspace' do
        expect(host_tags.length).to eq(tags.length)

        expect(
            host_tags.all? { |tag|
              tag.hosts.any? { |host|
                host.workspace == workspace
              }
            }
        ).to eq(true)
      end
    end

    context '#normalize' do
      let(:normalize) do
        workspace.send(:normalize)
      end

      before(:each) do
        workspace.boundary = boundary
      end

      context 'with boundary' do
        let(:boundary) do
          " #{stripped_boundary} "
        end

        let(:stripped_boundary) do
          '192.168.0.1'
        end

        it "should remove spaces" do
          normalize

          expect(workspace.boundary).to eq(stripped_boundary)
        end
      end

      context 'without boundary' do
        let(:boundary) do
          nil
        end

        it 'should not raise error' do
          expect {
            normalize
          }.to_not raise_error
        end
      end
    end

    context '#web_forms' do

      subject do
        workspace.web_forms
      end

      #
      # Let!s (let + before(:each))
      #

      let!(:other_web_forms) do
        other_web_sites.collect { |web_site|
          FactoryGirl.create(:web_form, :web_site => web_site)
        }
      end

      let!(:web_forms) do
        web_sites.collect { |web_site|
          FactoryGirl.create(:web_form, :web_site => web_site)
        }
      end

      it 'should return an ActiveRecord:Relation' do
        should be_a ActiveRecord::Relation
      end

      it 'should return only Mdm::WebPages from hosts in the workspace' do
        found_web_forms = workspace.web_forms

        expect(found_web_forms.length).to eq(web_forms.length)

        expect(
            found_web_forms.all? { |web_form|
              web_form.web_site.service.host.workspace == workspace
            }
        ).to eq(true)
      end
    end

    context '#web_sites' do
      subject do
        workspace.web_sites
      end

      #
      # Let!s (let + before(:each))
      #

      before(:each) do
        other_web_sites
        web_sites
      end

      it 'should return an ActiveRecord:Relation' do
        should be_a ActiveRecord::Relation
      end

      it 'should return only Mdm::WebVulns from hosts in the workspace' do
        # there are more web sites than those in the workspace
        expect(Mdm::WebSite.count).to be > web_sites.count

        found_web_sites = workspace.web_sites

        expect(found_web_sites.length).to eq(web_sites.count)

        expect(
            found_web_sites.all? { |web_site|
              web_site.service.host.workspace == workspace
            }
        ).to eq(true)
      end
    end

    context '#web_vulns' do
      subject do
        workspace.web_vulns
      end

      #
      # Let!s (let + before(:each))
      #

      let!(:other_web_vulns) do
        other_web_sites.collect { |web_site|
          FactoryGirl.create(:mdm_web_vuln, :web_site => web_site)
        }
      end

      let!(:web_vulns) do
        web_sites.collect { |web_site|
          FactoryGirl.create(:mdm_web_vuln, :web_site => web_site)
        }
      end

      it 'should return an ActiveRecord:Relation' do
        should be_a ActiveRecord::Relation
      end

      it 'should return only Mdm::WebVulns from hosts in the workspace' do
        expect(Mdm::WebVuln.count).to be > web_vulns.length

        found_web_vulns = workspace.web_vulns

        expect(found_web_vulns.length).to eq(web_vulns.length)

        expect(
            found_web_vulns.all? { |web_vuln|
              web_vuln.web_site.service.host.workspace == workspace
            }
        ).to eq(true)
      end
    end

    context '#web_unique_forms' do
      let(:rejected_address) do
        hosts[1].address
      end

      let(:selected_address) do
        hosts[0].address
      end

      it 'should return an ActiveRecord:Relation',
         :pending => 'https://www.pivotaltracker.com/story/show/43219917' do
        should be_a ActiveRecord::Relation
      end

      it "should reject #unique_web_forms from host addresses that aren't in addresses" do
        web_forms = workspace.web_unique_forms([selected_address])

        expect(
            web_forms.all? { |web_form|
              expect(web_form.web_site.service.host.address.to_s).to eq(selected_address)
            }
        ).to eq(true)
      end
    end
  end

end