cli
  1# frozen_string_literal: true
  2
  3RSpec.describe Scim::Kit::Http do
  4  subject { described_class.new }
  5
  6  let(:uri) { URI(FFaker::Internet.uri('https')) }
  7
  8  describe 'request logging' do
  9    let(:log) { StringIO.new }
 10    let(:server) { TCPServer.new('127.0.0.1', 0) }
 11
 12    around do |example|
 13      WebMock.disable!
 14      original = Scim::Kit.logger
 15      Scim::Kit.logger = Logger.new(log)
 16      described_class.instance_variable_set(:@default_driver, nil)
 17      example.run
 18      Scim::Kit.logger = original
 19      described_class.instance_variable_set(:@default_driver, nil)
 20      WebMock.enable!
 21      server.close
 22    end
 23
 24    def respond_once
 25      Thread.new do
 26        socket = server.accept
 27        loop { break if socket.gets.to_s.strip.empty? }
 28        socket.print("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n{}")
 29        socket.close
 30      end
 31    end
 32
 33    def fetch_with_credentials
 34      responder = respond_once
 35      described_class.new(retries: 0).fetch(
 36        URI("http://127.0.0.1:#{server.addr[1]}/Users"),
 37        headers: { 'Authorization' => 'Bearer s3cret' }
 38      )
 39      responder.join
 40    end
 41
 42    it 'keeps credentials out of the log' do
 43      fetch_with_credentials
 44
 45      expect(log.string).not_to include('s3cret')
 46    end
 47  end
 48
 49  describe '#fetch' do
 50    context 'when the response is successful' do
 51      let(:body) { { id: '123' } }
 52
 53      before { stub_request(:get, uri).to_return(status: 200, body: body.to_json) }
 54
 55      specify { expect(subject.fetch(uri)).to be_ok }
 56      specify { expect(subject.fetch(uri).status).to be(200) }
 57      specify { expect(subject.fetch(uri).body).to eql(body) }
 58    end
 59
 60    context 'when the response is a scim error' do
 61      let(:error_body) { { detail: 'Resource not found', status: '404' } }
 62
 63      before { stub_request(:get, uri).to_return(status: 404, body: error_body.to_json) }
 64
 65      specify { expect(subject.fetch(uri)).not_to be_ok }
 66      specify { expect(subject.fetch(uri).status).to be(404) }
 67      specify { expect(subject.fetch(uri).body).to eql(error_body) }
 68    end
 69
 70    context 'when the response body is not json' do
 71      before { stub_request(:get, uri).to_return(status: 500, body: 'boom') }
 72
 73      specify { expect(subject.fetch(uri)).not_to be_ok }
 74      specify { expect(subject.fetch(uri).body).to eql(detail: 'boom') }
 75    end
 76
 77    context 'when a successful response body is not json' do
 78      before { stub_request(:get, uri).to_return(status: 200, body: '<html>') }
 79
 80      specify { expect(subject.fetch(uri)).not_to be_ok }
 81      specify { expect(subject.fetch(uri).body).to eql(detail: '<html>') }
 82    end
 83
 84    context 'when the response has no body' do
 85      before { stub_request(:get, uri).to_return(status: 204, body: nil) }
 86
 87      specify { expect(subject.fetch(uri)).to be_ok }
 88      specify { expect(subject.fetch(uri).body).to eql({}) }
 89    end
 90
 91    context 'when the connection fails' do
 92      subject { described_class.new(retries: 0) }
 93
 94      before { stub_request(:get, uri).to_raise(Errno::ECONNREFUSED) }
 95
 96      specify { expect(subject.fetch(uri)).not_to be_ok }
 97      specify { expect(subject.fetch(uri).status).to be_nil }
 98      specify { expect(subject.fetch(uri).body[:detail]).to include('Connection refused') }
 99    end
100
101    context 'when headers are provided' do
102      before do
103        stub_request(:get, uri)
104          .with(headers: { 'X-Test' => 'value' })
105          .to_return(status: 200, body: '{}')
106      end
107
108      specify { expect(subject.fetch(uri, headers: { 'X-Test' => 'value' })).to be_ok }
109    end
110
111    context 'when the response redirects to the same origin' do
112      let(:credentials) { { 'Authorization' => 'Bearer xyz' } }
113      let(:redirect_uri) { URI.join(uri, '/v2/Users') }
114
115      before do
116        stub_request(:get, uri)
117          .to_return(status: 301, headers: { 'Location' => redirect_uri.to_s })
118        stub_request(:get, redirect_uri)
119          .with(headers: credentials).to_return(status: 200, body: '{}')
120      end
121
122      specify { expect(subject.fetch(uri, headers: credentials)).to be_ok }
123    end
124
125    context 'when the response redirects to another origin' do
126      let(:credentials) { { 'Authorization' => 'Bearer xyz' } }
127      let(:redirect_uri) { URI('https://elsewhere.example.com/Users') }
128
129      before do
130        stub_request(:get, uri)
131          .to_return(status: 301, headers: { 'Location' => redirect_uri.to_s })
132        stub_request(:get, redirect_uri).to_return(status: 200, body: '{}')
133      end
134
135      it 'does not forward the credentials' do
136        subject.fetch(uri, headers: credentials)
137
138        expect(a_request(:get, redirect_uri)
139          .with(headers: credentials)).not_to have_been_made
140      end
141    end
142  end
143
144  describe '#get' do
145    context 'when the response is successful' do
146      before { stub_request(:get, uri).to_return(status: 200, body: '{"a":1}') }
147
148      specify { expect(subject.get(uri)).to eql(a: 1) }
149    end
150
151    context 'when a successful response body is not json' do
152      before { stub_request(:get, uri).to_return(status: 200, body: '<html>') }
153
154      it 'does not hand the unparsed body to the caller' do
155        expect(subject.get(uri)).to eql({})
156      end
157    end
158  end
159end