Giter Club home page Giter Club logo

tcr's Introduction

TCR (TCP + VCR)

Build Status

TCR is a very lightweight version of VCR for TCP sockets.

Currently used for recording 'net/smtp', 'net/imap' and 'net/ldap' interactions so only a few of the TCPSocket methods are recorded out.

Installation

Add this line to your application's Gemfile:

gem 'tcr'

And then execute:

$ bundle

Or install it yourself as:

$ gem install tcr

Usage

require 'test/unit'
require 'tcr'

TCR.configure do |c|
  c.cassette_library_dir = 'fixtures/tcr_cassettes'
  c.hook_tcp_ports = [2525]
end

class TCRTest < Test::Unit::TestCase
  def test_example_dot_com
    TCR.use_cassette('mandrill_smtp') do
      tcp_socket = TCPSocket.open("smtp.mandrillapp.com", 2525)
      io = Net::InternetMessageIO.new(tcp_socket)
      assert_match /220 smtp.mandrillapp.com ESMTP/, io.readline
    end
  end
end

Run this test once, and TCR will record the tcp interactions to fixtures/tcr_cassettes/google_smtp.json.

[
  [
    [
      "read",
      "220 smtp.mandrillapp.com ESMTP\r\n"
    ]
  ]
]

Run it again, and TCR will replay the interactions from json when the tcp request is made. This test is now fast (no real TCP requests are made anymore), deterministic and accurate.

You can disable TCR hooking TCPSocket ports for a given block via turned_off:

TCR.turned_off do
  tcp_socket = TCPSocket.open("smtp.mandrillapp.com", 2525)
end

To make sure all external calls really happened use hit_all option:

class TCRTest < Test::Unit::TestCase
  def test_example_dot_com
    TCR.use_cassette('mandrill_smtp', hit_all: true) do
      # There are previously recorded external calls.
      # ExtraSessionsError will be raised as a result.
    end
  end
end

You can also use the configuration option:

TCR.configure do |c|
  c.hit_all = true
end

The following storage formats are supported:

  • JSON (default)
  • YAML
  • Marshal (recommended for binary data transfer like LDAP)

You can configure them via:

TCR.configure do |c|
  c.format = 'json'
  # or
  c.format = 'yaml'
  # or
  c.format = 'marshal'
end

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

tcr's People

Contributors

dependabot[bot] avatar gregdaniels813 avatar jch avatar olleolleolle avatar raszi avatar reidmorrison avatar rlue avatar robforman avatar sionide21 avatar thorsteneckel avatar yukas avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tcr's Issues

FTP connections raise ArgumentError: wrong number of arguments (given 2, expected 3)

Steps to reproduce problem

>> require 'tcr'
=> true
>> require 'net/ftp'
=> true
>> Net::FTP.open('root.cern.ch')
ArgumentError: wrong number of arguments (given 2, expected 3)
from ~/.rbenv/versions/2.4.4/lib/ruby/gems/2.4.0/gems/tcr-0.2.0/lib/tcr.rb:91:in `tcp'

(This error does not occur if you do not require 'tcr' first.)

The problem

TCR's monkey-patched Socket.tcp requires a different set of arguments from Ruby's built-in Socket.tcp:

# lib/ruby/2.5.0/socket.rb
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil)

# tcr/lib/tcr.rb
class Socket
  class << self
    def tcp(host, port, socket_opts)

Proposed solution

The socket_opts argument (not used by TCR, and passed directly to Socket.tcp for non-watched ports) should be modified to be optional / support multiple arguments with the splat operator (*):

def tcp(host, port, *socket_opts)

I'll submit a PR shortly.

Getting TCR::DirectionMismatchError after a successful run

Hi here! thanks for the work you put into this gem! I'm having a weird issue when replaying a cassette.

I'm using json format and hooking into these ports [25, 143, 465, 587, 993].

On the first run (when the cassette is generated) everything works perfectly, and when a re-run the test and in this case will load the cassette, I'm getting the error:

TCR::DirectionMismatchError: Expected to 'write' but next in recording was 'read'

On some simple cases I was able to edit the .json and the it works, but it defeat the purpose of the cassette, so I was wondering if at some point did you experience something similar, and in that case if you have any pointer in where to look for the issue or config.

Thanks a lot!

TCR is not threadsafe (and thus unusable for IMAP)

Ruby's native IMAP implementation uses threads โ€” below is an excerpt from the Net::IMAP initializer method:

def initialize
  ...
  @client_thread = Thread.current
  @receiver_thread = Thread.start {
    begin
      receive_responses
    rescue Exception
    end
  }
  @receiver_thread_terminating = false
rescue Exception
  ...
end

This leads to problems when recording/replaying cassettes: multiple threads record messages from different TCP transactions concurrently, with their timing staggered due to network latency. When the cassettes are replayed, the latency is eliminated, and messages are expected in an order different from the one they were recorded in.

As a result, cassettes are recorded to disk without error, but when replayed, tests fail with the following exception:

TCR::DirectionMismatchError:
  Expected to 'write' but next in recording was 'read'

(or vice versa)

1.9.3 compatibility to be maintained?

Seems travis is setup to test with 1.9.3 but it breaks, as I just discovered when I added it to a 1.9.3 project (I think mainly due to the refactoring on 0ef09a7)

Are there plans to maintain 1.9.3 compatibility? If not, I'd suggest remove 1.9.3 and make 2+ a requirement

Handling Shift-JIS Encoding

I am working on a application which requires handling Shift-JIS as opposed to UTF-8 encodings. I have found that saving the cassette to a file sometimes chokes (most likely due to the fact that the JSON library is expecting UTF-8).

By forking tcr and using yaml instead of json, I have been able to save the cassette data successfully. Is this capability something of general interest? Should I make a pull request?

RuntimeError Exception: can't modify frozen String

Hi @robforman - thanks for this great gem!

However, we encountered an issue with mocking an IMAP connection. TCR tries to force the UTF-8 encoding on frozen string which results in a RuntimeError Exception: can't modify frozen String.

Pull request is on the way ๐Ÿ‘

NoMethodError: undefined method `read_ber' for TCR::RecordableTCPSocket

I am using

  • gem 'tcr', github: 'robforman/tcr', tag: 'cf6b949ba0d98538bf6b3df0373001ef9ec3ef06' ie branch ruby-upgrade in Gemfile
  • ruby version: ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
  • rails version: 7.0.4.2
  • net-ldap 0.17.1

The error is:

Minitest::UnexpectedError: NoMethodError: undefined method `read_ber' for #<TCR::RecordableTCPSocket:0x00007f70f20f9d20 @read_lock=#<Thread::Queue:0x00007f70f20f9c80>, @recording=[["write", "...some stuff..."]], @live=true, @socket=#<TCPSocket:(closed)>>

        socket.read_ber(syntax) do |id, content_length|
              ^^^^^^^^^
Did you mean?  read_nonblock
    app/models/user.rb:55:in `ldap_client'

The code that is calling (app/models/user.rb) reads as follows:

  def self.ldap_client(user_name, password)
    auth = { method: :simple, username: sanitize_email(user_name), password: password }
    ldap = Net::LDAP.new(
      host: AD_HOST,
      auth: auth
    )
    # debugger
    ldap if ldap.bind  # Error thrown on bind (line 55)
  end

my test/test_helper.rb includes:

TCR.configure do |c|
  c.cassette_library_dir = File.expand_path('cassettes/tcr', __dir__)
  # ldap            389/tcp
  # ldaps           636/tcp
  c.hook_tcp_ports = ENV['DISABLE_TCR'].present? ? [] : [389, 636]
  if ENV['TCR_MODE'] =~ /rec/i
    c.hit_all = true
  end
end

FYI

  • It also reports an deprecation warning:
/home/vagrant/.gem/ruby/3.1.3/bundler/gems/tcr-cf6b949ba0d9/lib/tcr/cassette.rb:19: warning: File.exists? is deprecated; use File.exist? instead
  • I also end up with empty cassette files as per #37
  • For those that care, the user model is a thin shell around the LDAP response, not an active record class, just enough for current_user to return a User class, that responds to #admin? etc

Doesn't work with net-ldap gem

When trying to record an LDAP connection with the ruby-net-ldap gem, TCR raises the following exception:

Exceptions::UnprocessableEntity: Can't connect to 'localhost' on port '389', undefined method `read_ber' for #<TCR::RecordableTCPSocket:0x000055d461a080f8>

TCR is trying to mock out the object returned by the #socket method in the fourth line below โ€” but it looks like the method called on it is not implemented by the mock object:

def read(syntax = Net::LDAP::AsnSyntax)
  ber_object =
    instrument "read.net_ldap_connection", :syntax => syntax do |payload|
      socket.read_ber(syntax) do |id, content_length|
        payload[:object_type_id] = id
        payload[:content_length] = content_length
      end
    end

  return unless ber_object

  instrument "parse_pdu.net_ldap_connection" do |payload|
    pdu = payload[:pdu]  = Net::LDAP::PDU.new(ber_object)

    payload[:message_id] = pdu.message_id
    payload[:app_tag]    = pdu.app_tag

    pdu
  end
end

Sorry I can't offer more clarity; I'm still not too familiar with what TCR is doing under the hood. Happy to help continue debugging this issue with your guidance, though!

TCR fails if path to cassette does not exist

For instance,

>> TCR.configure do |c|
>*   c.cassette_library_dir = 'nonexistent/path'
>* end
=> "nonexistent/path"
>> TCR.use_cassette('test') { }
Errno::ENOENT: No such file or directory @ rb_sysopen - nonexistent/path/test.json

(I'm submitting a PR for this momentarily.)

Net::LDAP with LDAPS connection records empty cassettes

Cassettes are not being recorded when using Net::LDAP with an LDAPS connection (I didn't test it with plain LDAP), it only creates cassettes with an empty JSON array. It happens the same when using Marshall format. LDAP tests are only testing that using Net::LDAP does not raise an error, but it's not testing that a cassette is actually being recorded. Tested with net-ldap (0.16.2)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.