diff --git a/.rubocop.yml b/.rubocop.yml index 8dcf8d3..8f3e28a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,2 +1,2 @@ inherit_gem: - wetransfer_style: ruby/default.yml \ No newline at end of file + wetransfer_style: ruby/default.yml diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..097a15a --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.6.2 diff --git a/.travis.yml b/.travis.yml index 748429c..dabf05b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,12 +5,16 @@ rvm: - 2.4 - 2.5 - 2.6 -- jruby-9.0 +- jruby-9.0.5 +- jruby-9.1.6 +- jruby-9.2.0 before_install: echo Y | rvm @global do gem uninstall bundler ; gem install bundler -v 1.16.1 cache: bundler matrix: allow_failures: - - rvm: jruby-9.0 + - rvm: jruby-9.0.5 + - rvm: jruby-9.1.6 + - rvm: jruby-9.2.0 script: - 'if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then bundle exec rspec --exclude-pattern "spec/integration_spec.rb"; fi' - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bundle exec rspec; fi' diff --git a/README.md b/README.md index 7547066..49e5d94 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,10 @@ For your API key and additional info please visit our [developer portal](https:/ Add this line to your application's Gemfile: ```ruby -gem 'wetransfer', version: '0.9.0.beta2' +gem 'wetransfer', version: '0.10.0.beta1' + +# If you need Board support, as found in WeTransfer's Collect app, use version 0.9.x) +gem 'wetransfer', version: '0.9.0.beta3' ``` And then execute: @@ -57,7 +60,7 @@ Open the file in your text editor and add this line: WT_API_KEY= -Make sure to replace `` by your actual api key. Don't include the pointy brackets! +Make sure to replace `` with your actual api key. Don't include the pointy brackets! Great! Now you can go to your project file and use the client. @@ -67,7 +70,7 @@ A transfer is a collection of files that can be created once, and downloaded unt ```ruby # In your project file: -require 'we_transfer_client' +require 'we_transfer' client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) ``` @@ -75,29 +78,99 @@ client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) Now that you've got the client set up you can use `create_transfer_and_upload_files` to, well, create a transfer, and upload all files! ```ruby -transfer = client.create_transfer_and_upload_files(message: 'All the Things') do |upload| - upload.add_file_at(path: '/path/to/local/file.jpg') - upload.add_file_at(path: '/path/to/another/local/file.jpg') - upload.add_file(name: 'README.txt', io: StringIO.new("You should read All the Things!")) +transfer = client.create_transfer_and_upload_files(message: 'All the Things') do |transfer| + # Add a file using File.open. If you do it like this, :name and :io params are optional + transfer.add_file(io: File.open('Gemfile')) + + # Add a file with File.open, but give it a different name inside the transfer + transfer.add_file( + name: 'hello_world.rb', + io: File.open('path/to/file/with/different_name.rb') + ) + + # Using :name, :size and :io params. + # Specifying the size is not very useful in this case, but feel free to explicitly + # communicate the size of the coming io. + # + # The :name param is compulsory if it cannot be derived from the IO. + transfer.add_file( + name: 'README.txt', + size: 31, + io: StringIO.new("You should read All the Things!") + ) end # To get a link to your transfer, call `url` on your transfer object: -transfer.url => "https://we.tl/t-123234=" +transfer.url # => "https://we.tl/t-1232346" + +# Or inspect the whole transfer: +transfer.to_h # => +# { +# :id => "0d5ce492c0cd935b5376c7858b0ff5ae20190307162739", +# :state => "processing", +# :url => "https://we.tl/t-CVINGH30C4", +# :message => "test transfer", +# :files => [ +# { +# :name => "README.txt", +# :size => 31, +# :id => "0e04833491a31776770ac4dcf83d1f4a20190307162739", +# :multipart => { +# :chunks => 1, +# :chunk_size => 31 +# } +# }, { +# :name => "hello_world.rb", +# :size => 166, +# :id => "22423dd4b44300641a4659203ba5d1bb20190307162739", +# :multipart => { +# :chunks => 1, +# :chunk_size => 166 +# } +# } +# ] +# } ``` The upload will be performed at the end of the block. Depending on your file sizes and network connection speed, this might take some time. What are you waiting for? Open that link in your browser! Chop chop. +If you want to have more control over which files uploads when, it is also possible. + +```ruby +# Create a transfer that consists of 1 file. +transfer = client.create_transfer(message: "test transfer") do |transfer| + # When creating a transfer, at least the name and the size of the file(s) + # must be known. + transfer.add_file(name: "small_file", size: 80) +end + +# Upload the file. The Ruby SDK will upload the file in chunks. +transfer.upload_file(name: "small_file", io: StringIO.new("#" * 80)) + +# Mark the file as completely uploaded. All the chunks of the file will be joined +# together to recreate the file +transfer.complete_file(name: "small_file") + +# Mark the transfer as completely done, so your customers can start downloading it +transfer.finalize + +# Inspect your transfer. Use transfer.to_h or transfer.to_json, depending on your scenario +transfer.to_h +``` + ## Boards +**NOTE**: **Boards are disabled from version 0.10.x** of the Ruby SDK. The latest releases that include **board support is found in version 0.9.x** + A board is a collection of files and links, but it is open for modifications. Like your portfolio: While working, you can make adjustments to it. A board is a fantastic place for showcasing your work in progress. Boards need a WeTransfer Client to be present, just like transfers. ```ruby # In your project file: -require 'we_transfer_client' +require 'we_transfer' client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) ``` @@ -106,10 +179,10 @@ After you create your client, you can ### Create a board and upload items - ```ruby board = client.create_board(name: 'Meow', description: 'On Cats') do |items| - items.add_file(name: 'big file.jpg', io: File.open('/path/to/huge_file.jpg', 'rb')items.add_file_at(path: '/path/to/another/file.txt') + items.add_file(name: 'big file.jpg', io: File.open('/path/to/huge_file.jpg') + items.add_file_at(path: '/path/to/another/file.txt') items.add_web_url(url: 'http://wepresent.wetransfer.com', title: 'Time well spent') end @@ -150,7 +223,7 @@ Hooray! ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/wetransfer/wetransfer_ruby_sdk. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. More extensive contribution guidelines can be found [here](https://github.com/WeTransfer/wetransfer_ruby_sdk/blob/master/.github/CONTRIBUTING.md). +Bug reports and pull requests are welcome on GitHub at This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. More extensive contribution guidelines can be found [here](https://github.com/WeTransfer/wetransfer_ruby_sdk/blob/master/.github/CONTRIBUTING.md). ## License @@ -158,4 +231,4 @@ The gem is available as open source under the terms of the [MIT License](https:/ ## Code of Conduct -Everyone interacting in the WeTransfer Ruby SDK project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/WeTransfer/wetransfer_ruby_sdk/blob/master/.github/CODE_OF_CONDUCT.md). +Everyone interacting in the WeTransfer Ruby SDK project’s code bases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/WeTransfer/wetransfer_ruby_sdk/blob/master/.github/CODE_OF_CONDUCT.md). diff --git a/bin/console b/bin/console index 47e3383..7b440e3 100755 --- a/bin/console +++ b/bin/console @@ -1,7 +1,7 @@ #!/usr/bin/env ruby require 'bundler/setup' -require 'we_transfer_client' +require 'we_transfer' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. diff --git a/examples/create_collection.rb b/examples/create_collection.rb index 2596d38..bbcf629 100644 --- a/examples/create_collection.rb +++ b/examples/create_collection.rb @@ -1,4 +1,4 @@ -require_relative 'we_transfer_client' +require_relative 'we_transfer' require 'dotenv' Dotenv.load diff --git a/examples/create_transfer.rb b/examples/create_transfer.rb index 2596d38..bbcf629 100644 --- a/examples/create_transfer.rb +++ b/examples/create_transfer.rb @@ -1,4 +1,4 @@ -require_relative 'we_transfer_client' +require_relative 'we_transfer' require 'dotenv' Dotenv.load diff --git a/lib/we_transfer.rb b/lib/we_transfer.rb new file mode 100644 index 0000000..9afb6ce --- /dev/null +++ b/lib/we_transfer.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'faraday' +require 'logger' +require 'json' +require 'ks' + +%w[ + version + logging + communicator + client + transfer + mini_io + we_transfer_file + remote_file version +].each do |file| + require_relative "we_transfer/#{file}" +end + +module WeTransfer + NULL_LOGGER = Logger.new(nil) + + def self.logger + @logger || NULL_LOGGER + end + + # Set the logger to your preferred logger + # + # @param new_logger [Logger] the logger that WeTransfer SDK should use + # + # @example Send all logs to Rails logger + # WeTransfer.logger = Rails.logger + def self.logger=(new_logger) + @logger = new_logger + end +end diff --git a/lib/we_transfer/client.rb b/lib/we_transfer/client.rb new file mode 100644 index 0000000..06bf0cc --- /dev/null +++ b/lib/we_transfer/client.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module WeTransfer + class Client + class Error < StandardError; end + extend Forwardable + + # Initialize a WeTransfer::Client + # + # @param api_key [String] The API key you want to authenticate with + # + # @return [WeTransfer::Client] + def initialize(api_key:) + @communicator = Communicator.new(api_key) + end + + def create_transfer(**args, &block) + transfer = WeTransfer::Transfer.new(args.merge(communicator: @communicator)) + transfer.persist(&block) + end + + def create_transfer_and_upload_files(**args, &block) + transfer = create_transfer(args, &block) + transfer + .upload_files + .complete_files + .finalize + end + + def_delegator :@communicator, :find_transfer + end +end diff --git a/lib/we_transfer/communicator.rb b/lib/we_transfer/communicator.rb new file mode 100644 index 0000000..9d96bdb --- /dev/null +++ b/lib/we_transfer/communicator.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +module WeTransfer + class CommunicationError < StandardError; end + + class Communicator + include Logging + extend Forwardable + + API_URL_BASE = "https://dev.wetransfer.com" + AUTHORIZE_URI = "/v2/authorize" + FINALIZE_URI = "/v2/transfers/%s/finalize" + TRANSFER_URI = "/v2/transfers/%s" + TRANSFERS_URI = "/v2/transfers" + UPLOAD_URL_URI = "/v2/transfers/%s/files/%s/upload-url/%s" + + DEFAULT_HEADERS = { + "User-Agent" => "WetransferRubySdk/#{WeTransfer::VERSION} Ruby #{RUBY_VERSION}", + "Content-Type" => "application/json" + }.freeze + + def initialize(api_key) + @api_key = api_key + end + + # Send a request to WeTransfer's Public API, resulting in the instantiation of a transfer + # + # @param transfer_id [String] The id of the transfer, as generated by + # the WeTransfer Public API, that you want to find. + # + # @raise [WeTransfer::CommunicationError] If the transfer cannot be found + # + # @return [WeTransfer::Transfer] the transfer you requested + # + def find_transfer(transfer_id) + response = ensure_ok_status!(request_as.get(TRANSFER_URI % transfer_id)) + + response_body = remote_transfer_params(response.body) + found_transfer = Transfer.new(message: response_body[:message], communicator: self) + setup_transfer( + transfer: found_transfer, + data: response_body + ) + found_transfer + end + + # GET an URL we can PUT a chunk of a file to. + # Note that this URL is valid for one hour, so if a lot of chunks have to be upload + # (or a slow network is expected), take care to not request your URLs too early + # + # @param transfer_id [String] the id of the transfer the file belongs to + # @param file_id [String] the id of the file the URL is requested for + # @param chunk [String, Number] the (1 based) chunk number of the file + # + # @raise [WeTransfer::CommunicationError] if the request to the WeTransfer Public API + # cannot be satisfied (e.g. transfer not found, file id unknown, chunk out of bound) + # + # @return [String] a signed URL, valid for an hour + # + def upload_url_for_chunk(transfer_id, file_id, chunk) + response = ensure_ok_status!(request_as.get(UPLOAD_URL_URI % [transfer_id, file_id, chunk])) + + JSON.parse(response.body).fetch("url") + end + + # Send a request to WeTransfer's Public API to create a transfer with + # a message and some files + # + # @param [Transfer] the transfer that should persist on WeTransfer + # + # @raise [WeTransfer::CommunicationError] if the request to the WeTransfer Public API + # cannot be satisfied (e.g. transfer has no message, files with duplicate names) + # + # @return [WeTransfer::Transfer] the transfer as persisted on WeTransfer. Persisting a transfer + # changes the state of the transfer, so it is the same object that was sent in as param, + # but some instance variables will be different. + # + def persist_transfer(transfer) + response = ensure_ok_status!( + request_as.post( + TRANSFERS_URI, + transfer.as_persist_params.to_json, + ) + ) + + handle_remote_transfer_data( + transfer: transfer, + data: remote_transfer_params(response.body) + ) + end + + # Send a request to WeTransfer's Public API to signal that the transfer should + # be locked and processed so it can be downloaded. + # + # @param [Transfer] transfer The transfer that should be finalized + # + # @raise [WeTransfer::CommunicationError] if the request to the WeTransfer Public API + # cannot be satisfied (e.g. not all chunks are uploaded, too much or too little data + # is uploaded) + # + # @return [WeTransfer::Transfer] The transfer. This is the same object that was send in through + # the param, but some local state will be updated according to the result of the call + # + def finalize_transfer(transfer) + response = ensure_ok_status!(request_as.put(FINALIZE_URI % transfer.id)) + handle_remote_transfer_data( + transfer: transfer, + data: remote_transfer_params(response.body) + ) + end + + def upload_chunk(put_url, chunk_contents) + @chunk_uploader ||= Faraday.new { |c| minimal_faraday_config(c) } + + @chunk_uploader.put( + put_url, + chunk_contents.read, + 'Content-Type' => 'binary/octet-stream', + 'Content-Length' => chunk_contents.size.to_s + ) + end + + def complete_file(transfer_id, file_id, chunks) + response = ensure_ok_status!( + request_as.put( + "/v2/transfers/%s/files/%s/upload-complete" % [transfer_id, file_id], + { part_numbers: chunks }.to_json + ) + ) + + remote_transfer_params(response.body) + end + + private + + def remote_transfer_params(response_body) + JSON.parse(response_body, symbolize_names: true) + end + + def request_as + @request_as ||= Faraday.new(API_URL_BASE) do |c| + minimal_faraday_config(c) + c.headers = auth_headers.merge DEFAULT_HEADERS + end + end + + def setup_transfer(transfer:, data:) + data[:files].each do |file_params| + transfer.add_file( + name: file_params[:name], + size: file_params[:size], + ) + end + + handle_remote_transfer_data(transfer: transfer, data: data) + end + + def handle_remote_transfer_data(transfer:, data:) + %i[id state url].each do |i_var| + transfer.instance_variable_set "@#{i_var}", data[i_var] + end + + RemoteFile.upgrade( + transfer: transfer, + files_response: data[:files] + ) + + transfer + end + + def auth_headers + authorize_if_no_bearer_token! + + { + 'X-API-Key' => @api_key, + 'Authorization' => "Bearer #{@bearer_token}" + } + end + + def ensure_ok_status!(response) + return response if (200..299).include?(response.status) + + logger.error(response) + case response.status + when 400..499 + raise WeTransfer::CommunicationError, "Response had a #{response.status} status code, message was '#{JSON.parse(response.body)["message"]}'." + when 500..504 + raise WeTransfer::CommunicationError, "Response had a #{response.status} status code, we could retry" + end + raise WeTransfer::CommunicationError, "Response had a #{response.status} status code, no idea what to do with that" + end + + def authorize_if_no_bearer_token! + return @bearer_token if @bearer_token + + response = ensure_ok_status!( + Faraday.new(API_URL_BASE) do |c| + minimal_faraday_config(c) + c.headers = DEFAULT_HEADERS.merge('X-API-Key' => @api_key) + end.post(AUTHORIZE_URI) + ) + bearer_token = JSON.parse(response.body)['token'] + raise WeTransfer::CommunicationError, "The authorization call returned #{response.body} and no usable :token key could be found there" if bearer_token.nil? || bearer_token.empty? + @bearer_token = bearer_token + end + + def minimal_faraday_config(config) + config.response :logger, logger + config.adapter Faraday.default_adapter + end + end +end diff --git a/lib/we_transfer/logging.rb b/lib/we_transfer/logging.rb new file mode 100644 index 0000000..8b9b9ec --- /dev/null +++ b/lib/we_transfer/logging.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module WeTransfer + module Logging + def logger + WeTransfer.logger + end + end +end diff --git a/lib/we_transfer/mini_io.rb b/lib/we_transfer/mini_io.rb new file mode 100644 index 0000000..6c86614 --- /dev/null +++ b/lib/we_transfer/mini_io.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module WeTransfer + # Wrapper around an IO object, delegating only methods we need for creating + # and sending a file in chunks. + # + class MiniIO + extend Forwardable + + def self.mini_io_able?(io) + return false if io.is_a? WeTransfer::NullMiniIO + + io.seek(0) + io.read(1) + io.rewind + io.size + true + rescue + false + end + + # Make sure MiniIO does not wrap a MiniIO instance + # + # @param io [anything] An object that responds to #read, #rewind, #seek, #size + # + def self.new(io) + return WeTransfer::NullMiniIO.instance if io.nil? + return io if io.is_a? WeTransfer::MiniIO + + super + end + + # Initialize with an io object to wrap + # + # @param io [anything] An object that responds to #read, #rewind, #seek, #size + # + def initialize(io) + ensure_mini_io_able!(io) + + @io = io + @io.rewind + end + + # The name of the io, guessed using File.basename. If this raises a TypeError + # we swallow the error, since this is used only as fallback for naming a + # WeTransferFile + # + def name + File.basename(@io) + rescue TypeError + end + + def_delegators :@io, :read, :rewind, :seek, :size + + private + + def ensure_mini_io_able!(io) + return if self.class.mini_io_able?(io) + + raise ArgumentError, "The io must respond to seek(), read(), size() and rewind(), but #{io.inspect} did not" + end + end + + class NullMiniIO + require 'singleton' + include Singleton + + instance.freeze + + def read(*); end + + def rewind; end + + def seek(*); end + + def size; end + + def name; end + end +end diff --git a/lib/we_transfer/remote_file.rb b/lib/we_transfer/remote_file.rb new file mode 100644 index 0000000..82b0ab4 --- /dev/null +++ b/lib/we_transfer/remote_file.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module WeTransfer + module RemoteFile + class FileMismatchError < StandardError; end + class NoIoError < StandardError; end + + class Multipart < ::Ks.strict(:chunks, :chunk_size); end + + def self.upgrade(files_response:, transfer:) + files_response.each do |file_response| + local_file = transfer.find_file(file_response[:name]) + + local_file.instance_variable_set( + :@id, + file_response[:id] + ) + + local_file.instance_variable_set( + :@multipart, + Multipart.new( + chunks: file_response[:multipart][:part_numbers], + chunk_size: file_response[:multipart][:chunk_size], + ) + ) + end + end + end +end diff --git a/lib/we_transfer/transfer.rb b/lib/we_transfer/transfer.rb new file mode 100644 index 0000000..769c467 --- /dev/null +++ b/lib/we_transfer/transfer.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +module WeTransfer + class Transfer + class DuplicateFileNameError < ArgumentError; end + class NoFilesAddedError < StandardError; end + class FileMismatchError < StandardError; end + + extend Forwardable + + attr_reader :files, :id, :state, :url, :message + + def self.create(message:, communicator:, &block) + transfer = new(message: message, communicator: communicator) + + transfer.persist(&block) + end + + def initialize(message:, communicator:) + @message = message + @communicator = communicator + @files = [] + @unique_file_names = Set.new + end + + def persist + yield(self) if block_given? + raise NoFilesAddedError if @unique_file_names.empty? + + @communicator.persist_transfer(self) + end + + # Add a file to a transfer. + # + # @param args [Hash] (See WeTransferFile#initialize) + # + # @raise [DuplicateFileNameError] Files should have a unique name - + # case insensitive - within a transfer. If that is not true, it + # will result in this error + # + # @return [WeTransfer::Transfer] self + # + # @see WeTransferFile#initialize + # + def add_file(**args) + file = WeTransferFile.new(args) + raise DuplicateFileNameError unless @unique_file_names.add?(file.name.downcase) + + @files << file + self + end + + # Upload the file. Convenience method for uploading the parts of the file in + # chunks. This will upload all chunks in order, single threaded. + # + # @param :name [String] The name used to add the file to the transfer + # @param :io [optional, String] The contents to be uploaded. If the file was + # added (See Transfer#add_file) including an io, this can be omitted. + # If the file was added including an io, *and* it is included in this + # call, the io from this invocation will be uploaded. + # @param :file [optional, WeTransferFile] The file instance that will be + # uploaded. + # + # @raise [WeTransfer::RemoteFile::NoIoError] Will be raised if the io does + # not meet the minimal requirements (see MiniIo.mini_io_able?) + # @example + # + # @see MiniIo.mini_io_able? + def upload_file(name:, io: nil, file: nil) + file ||= find_file(name) + put_io = io || file.io + + raise( + WeTransfer::RemoteFile::NoIoError, + "IO for file with name '#{name}' cannot be uploaded." + ) unless WeTransfer::MiniIO.mini_io_able?(put_io) + (1..file.multipart.chunks).each do |chunk| + put_url = upload_url_for_chunk(file_id: file.id, chunk: chunk) + chunk_contents = StringIO.new(put_io.read(file.multipart.chunk_size)) + chunk_contents.rewind + + @communicator.upload_chunk(put_url, chunk_contents) + end + end + + # Trigger the upload for all files. Since this method does not accept any io, + # the files should be added (see #add_file) with a proper io object. + # + # @return [WeTransfer::Transfer] self + def upload_files + files.each do |file| + upload_file( + name: file.name, + file: file + ) + end + self + end + + def upload_url_for_chunk(file_id: nil, name: nil, chunk:) + raise ArgumentError, "missing keyword: either name or file_id is required" unless file_id || name + + file_id ||= find_file(name).id + @communicator.upload_url_for_chunk(id, file_id, chunk) + end + + def complete_file(file: nil, name: file&.name, file_id: file&.id) + raise ArgumentError, "missing keyword: either name or file is required" unless file || name || file_id + + file ||= find_file(name || file_id) + @communicator.complete_file(id, file.id, file.multipart.chunks) + end + + def complete_files + files.each do |file| + complete_file( + name: file.name, + file: file + ) + end + self + end + + def finalize + @communicator.finalize_transfer(self) + end + + def as_persist_params + { + message: @message, + files: @files.map(&:as_persist_params), + } + end + + def to_json + to_h.to_json + end + + def to_h + prepared = %i[id state url message].each_with_object({}) do |prop, memo| + memo[prop] = send(prop) + end + + prepared[:files] = files.map(&:to_h) + prepared + end + + # Find a file inside this transfer + # + # @param [String] name_or_id, The name (as set by you) or the id (as returned) + # by WeTransfer Public API of the file. + # @raise [FileMismatchError] if a file with that name or id cannot be found for + # this transfer + # @return [WeTransferFile] The file you requested + # + def find_file(name_or_id) + @found_files ||= Hash.new do |h, name_or_id| + h[name_or_id] = files.find { |file| [file.name, file.id].include? name_or_id } + end + + raise FileMismatchError unless @found_files[name_or_id] + @found_files[name_or_id] + end + end +end diff --git a/lib/we_transfer/version.rb b/lib/we_transfer/version.rb new file mode 100644 index 0000000..3394081 --- /dev/null +++ b/lib/we_transfer/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module WeTransfer + VERSION = '0.10.0.beta1' +end diff --git a/lib/we_transfer/we_transfer_file.rb b/lib/we_transfer/we_transfer_file.rb new file mode 100644 index 0000000..27ba0c2 --- /dev/null +++ b/lib/we_transfer/we_transfer_file.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module WeTransfer + class WeTransferFile + attr_reader :name, :id, :io, :multipart, :size + + # Construct a WeTransferFile + # + # The initializer will try its best to figure out the name and size if it + # isn't provided, but the io can provide it. + # + # @param :io [optional, anything] The io of the file. This will be wrapped + # in a MiniIo, or if absent in a NullMiniIo + # @param :name [optional, String] The name you want to give your file. This + # does not have to match the original file name. + # @param :size [optional, Numeric] The size of the file. Has to be exact to + # the byte. If omitted, the io will be used to figure out the size + # + # @raise [ArgumentError] If :name and :size arguments are empty and cannot + # be derived from the io. + # + # @example With only an io. This will figure out the name and size of the file using MiniIo. + # WeTransfer::WeTransferFile.new(io: File.open('Gemfile')) # => + # #>, + # @name="Gemfile", + # @size=166 + # > + # + # @example With only a name and size. + # WeTransfer::WeTransferFile.new(name: 'README.md', size: 1337) # => + # #, + # @name="README.md", + # @size=1337 + # > + # + # @see MiniIo.name + # @see MiniIo.size + # @see NullMiniIo + # + def initialize(name: nil, size: nil, io: nil) + @io = MiniIO.new(io) + @name = name || @io.name + @size = size || @io.size + + raise ArgumentError, "Need a file name and a size, or io should provide it" unless @name && @size + end + + def as_persist_params + { + name: @name, + size: @size, + } + end + + def to_h + prepared = %i[name size id].each_with_object({}) do |prop, memo| + memo[prop] = send(prop) + end + prepared[:multipart] = multipart.to_h + + prepared + end + end +end diff --git a/lib/we_transfer_client.rb b/lib/we_transfer_client.rb deleted file mode 100644 index b9b8371..0000000 --- a/lib/we_transfer_client.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'faraday' -require 'logger' -require 'json' - -require_relative 'we_transfer_client/version' -require_relative 'we_transfer_client/transfer_builder' -require_relative 'we_transfer_client/board_builder' -require_relative 'we_transfer_client/future_file' -require_relative 'we_transfer_client/future_link' -require_relative 'we_transfer_client/future_transfer' -require_relative 'we_transfer_client/future_board' -require_relative 'we_transfer_client/remote_transfer' -require_relative 'we_transfer_client/remote_board' -require_relative 'we_transfer_client/remote_link' -require_relative 'we_transfer_client/remote_file' -require_relative 'we_transfer_client/transfers' -require_relative 'we_transfer_client/boards' - -module WeTransfer - class Client - include WeTransfer::Client::Transfers - include WeTransfer::Client::Boards - - class Error < StandardError - end - - NULL_LOGGER = Logger.new(nil) - - def initialize(api_key:, logger: NULL_LOGGER) - @api_url_base = 'https://dev.wetransfer.com' - @api_key = api_key.to_str - @bearer_token = nil - @logger = logger - end - - def upload_file(object:, file:, io:) - put_io_in_parts(object: object, file: file, io: io) - end - - def complete_file!(object:, file:) - object.prepare_file_completion(client: self, file: file) - end - - def check_for_file_duplicates(files, new_file) - if files.select { |file| file.name == new_file.name }.size != 1 - raise ArgumentError, 'Duplicate file entry' - end - end - - def put_io_in_parts(object:, file:, io:) - (1..file.multipart.part_numbers).each do |part_n_one_based| - upload_url, chunk_size = object.prepare_file_upload(client: self, file: file, part_number: part_n_one_based) - part_io = StringIO.new(io.read(chunk_size)) - part_io.rewind - response = faraday.put( - upload_url, - part_io, - 'Content-Type' => 'binary/octet-stream', - 'Content-Length' => part_io.size.to_s - ) - ensure_ok_status!(response) - end - {success: true, message: 'File Uploaded'} - end - - def faraday - Faraday.new(@api_url_base) do |c| - c.response :logger, @logger - c.adapter Faraday.default_adapter - c.headers = { 'User-Agent' => "WetransferRubySdk/#{WeTransfer::VERSION} Ruby #{RUBY_VERSION}"} - end - end - - def authorize_if_no_bearer_token! - return if @bearer_token - response = faraday.post('/v2/authorize', '{}', 'Content-Type' => 'application/json', 'X-API-Key' => @api_key) - ensure_ok_status!(response) - @bearer_token = JSON.parse(response.body, symbolize_names: true)[:token] - if @bearer_token.nil? || @bearer_token.empty? - raise Error, "The authorization call returned #{response.body} and no usable :token key could be found there" - end - end - - def auth_headers - raise 'No bearer token retrieved yet' unless @bearer_token - { - 'X-API-Key' => @api_key, - 'Authorization' => ('Bearer %s' % @bearer_token), - } - end - - def ensure_ok_status!(response) - case response.status - when 200..299 - true - when 400..499 - @logger.error response - raise Error, "Response had a #{response.status} code, the server will not accept this request even if retried" - when 500..504 - @logger.error response - raise Error, "Response had a #{response.status} code, we could retry" - else - @logger.error response - raise Error, "Response had a #{response.status} code, no idea what to do with that" - end - end - end -end diff --git a/lib/we_transfer_client/board_builder.rb b/lib/we_transfer_client/board_builder.rb deleted file mode 100644 index d5444b5..0000000 --- a/lib/we_transfer_client/board_builder.rb +++ /dev/null @@ -1,33 +0,0 @@ -class BoardBuilder - attr_reader :items - class TransferIOError < StandardError; end - - def initialize - @items = [] - end - - def add_file(name:, io:) - ensure_io_compliant!(io) - @items << FutureFile.new(name: name, io: io) - end - - def add_file_at(path:) - add_file(name: File.basename(path), io: File.open(path, 'rb')) - end - - def add_web_url(url:, title: url) - @items << FutureLink.new(url: url, title: title) - end - - private - - def ensure_io_compliant!(io) - io.seek(0) - io.read(1) # Will cause things like Errno::EACCESS to happen early, before the upload begins - io.seek(0) # Also rewinds the IO for later uploading action - size = io.size # Will cause a NoMethodError - raise TransferIOError, "#{File.basename(io)}, given to add_file has a size of 0" if size <= 0 - rescue NoMethodError - raise TransferIOError, "#{File.basename(io)}, given to add_file must respond to seek(), read() and size(), but #{io.inspect} did not" - end -end diff --git a/lib/we_transfer_client/boards.rb b/lib/we_transfer_client/boards.rb deleted file mode 100644 index 3444eef..0000000 --- a/lib/we_transfer_client/boards.rb +++ /dev/null @@ -1,74 +0,0 @@ -module WeTransfer - class Client - module Boards - def create_board_and_upload_items(name:, description:, &block) - future_board = create_feature_board(name: name, description: description, &block) - remote_board = create_remote_board(board: future_board) - remote_board.files.each do |file| - check_for_file_duplicates(future_board.files, file) - local_file = future_board.files.select { |x| x.name == file.name }.first - upload_file(object: remote_board, file: file, io: local_file.io) - complete_file!(object: remote_board, file: file) - end - remote_board - end - - def get_board(board:) - request_board(board: board) - end - - private - - def create_board(name:, description:, &block) - future_board = create_feature_board(name: name, description: description, &block) - create_remote_board(board: future_board) - end - - def add_items(board:, board_builder_class: BoardBuilder) - builder = board_builder_class.new - yield(builder) - add_items_to_remote_board(items: builder.items, remote_board: board) - rescue LocalJumpError - raise ArgumentError, 'No items where added to the board' - end - - def create_feature_board(name:, description:, future_board_class: FutureBoard, board_builder_class: BoardBuilder) - builder = board_builder_class.new - yield(builder) if block_given? - future_board_class.new(name: name, description: description, items: builder.items) - end - - def create_remote_board(board:, remote_board_class: RemoteBoard) - authorize_if_no_bearer_token! - response = faraday.post( - '/v2/boards', - JSON.pretty_generate(board.to_initial_request_params), - auth_headers.merge('Content-Type' => 'application/json') - ) - ensure_ok_status!(response) - remote_board = remote_board_class.new(JSON.parse(response.body, symbolize_names: true)) - board.items.any? ? add_items_to_remote_board(items: board.items, remote_board: remote_board) : remote_board - end - - def add_items_to_remote_board(items:, remote_board:) - items.group_by(&:class).each do |_, grouped_items| - grouped_items.each do |item| - item.add_to_board(client: self, remote_board: remote_board) - end - end - remote_board - end - - def request_board(board:, remote_board_class: RemoteBoard) - authorize_if_no_bearer_token! - response = faraday.get( - "/v2/boards/#{board.id}", - {}, - auth_headers.merge('Content-Type' => 'application/json') - ) - ensure_ok_status!(response) - remote_board_class.new(JSON.parse(response.body, symbolize_names: true)) - end - end - end -end diff --git a/lib/we_transfer_client/future_board.rb b/lib/we_transfer_client/future_board.rb deleted file mode 100644 index 15f2042..0000000 --- a/lib/we_transfer_client/future_board.rb +++ /dev/null @@ -1,32 +0,0 @@ -class FutureBoard - attr_reader :name, :description, :items - - def initialize(name:, description: nil, items: []) - @name = name - @description = description - @items = items - end - - def files - @items.select { |item| item.class == FutureFile } - end - - def links - @items.select { |item| item.class == FutureLink } - end - - def to_initial_request_params - { - name: name, - description: description, - } - end - - def to_request_params - { - name: name, - description: description, - items: items.map(&:to_request_params), - } - end -end diff --git a/lib/we_transfer_client/future_file.rb b/lib/we_transfer_client/future_file.rb deleted file mode 100644 index f98780d..0000000 --- a/lib/we_transfer_client/future_file.rb +++ /dev/null @@ -1,28 +0,0 @@ -class FutureFile - attr_reader :name, :io - - def initialize(name:, io:) - @name = name - @io = io - end - - def to_request_params - { - name: @name, - size: @io.size, - } - end - - def add_to_board(client:, remote_board:) - client.authorize_if_no_bearer_token! - response = client.faraday.post( - "/v2/boards/#{remote_board.id}/files", - # this needs to be a array with hashes => [{name, filesize}] - JSON.pretty_generate([to_request_params]), - client.auth_headers.merge('Content-Type' => 'application/json') - ) - client.ensure_ok_status!(response) - file_item = JSON.parse(response.body, symbolize_names: true).first - remote_board.items << RemoteFile.new(file_item) - end -end diff --git a/lib/we_transfer_client/future_link.rb b/lib/we_transfer_client/future_link.rb deleted file mode 100644 index b158bc4..0000000 --- a/lib/we_transfer_client/future_link.rb +++ /dev/null @@ -1,28 +0,0 @@ -class FutureLink - attr_reader :url, :title - - def initialize(url:, title: url) - @url = url - @title = title - end - - def to_request_params - { - url: url, - title: title, - } - end - - def add_to_board(client:, remote_board:) - client.authorize_if_no_bearer_token! - response = client.faraday.post( - "/v2/boards/#{remote_board.id}/links", - # this needs to be a array with hashes => [{name, filesize}] - JSON.pretty_generate([to_request_params]), - client.auth_headers.merge('Content-Type' => 'application/json') - ) - client.ensure_ok_status!(response) - file_item = JSON.parse(response.body, symbolize_names: true).first - remote_board.items << RemoteLink.new(file_item) - end -end diff --git a/lib/we_transfer_client/future_transfer.rb b/lib/we_transfer_client/future_transfer.rb deleted file mode 100644 index 37a1c75..0000000 --- a/lib/we_transfer_client/future_transfer.rb +++ /dev/null @@ -1,15 +0,0 @@ -class FutureTransfer - attr_accessor :message, :files - - def initialize(message:, files: []) - @message = message - @files = files - end - - def to_request_params - { - message: message, - files: files.map(&:to_request_params), - } - end -end diff --git a/lib/we_transfer_client/remote_board.rb b/lib/we_transfer_client/remote_board.rb deleted file mode 100644 index 3e3c4d6..0000000 --- a/lib/we_transfer_client/remote_board.rb +++ /dev/null @@ -1,47 +0,0 @@ -class RemoteBoard - ItemTypeError = Class.new(NameError) - - attr_reader :id, :items, :url, :state - - CHUNK_SIZE = 6 * 1024 * 1024 - - def initialize(id:, state:, url:, name:, description: '', items: [], **) - @id = id - @state = state - @url = url - @name = name - @description = description - @items = to_instances(items: items) - end - - def prepare_file_upload(client:, file:, part_number:) - url = file.request_board_upload_url(client: client, board_id: @id, part_number: part_number) - [url, CHUNK_SIZE] - end - - def prepare_file_completion(client:, file:) - file.complete_board_file(client: client, board_id: @id) - end - - def files - @items.select { |item| item.class == RemoteFile } - end - - def links - @items.select { |item| item.class == RemoteLink } - end - - private - - def to_instances(items:) - items.map do |item| - begin - remote_class = "Remote#{item[:type].capitalize}" - Module.const_get(remote_class) - .new(item) - rescue NameError - raise ItemTypeError, "Cannot instantiate item with type '#{item[:type]}' and id '#{item[:id]}'" - end - end - end -end diff --git a/lib/we_transfer_client/remote_file.rb b/lib/we_transfer_client/remote_file.rb deleted file mode 100644 index ce69c9e..0000000 --- a/lib/we_transfer_client/remote_file.rb +++ /dev/null @@ -1,55 +0,0 @@ -class RemoteFile - attr_reader :multipart, :name, :id, :url, :type - - def initialize(id:, name:, size:, url: nil, type: 'file', multipart:) - @id = id - @name = name - @size = size - @url = url - @type = type - @size = size - multi = Struct.new(*multipart.keys) - @multipart = multi.new(*multipart.values) - end - - def request_transfer_upload_url(client:, transfer_id:, part_number:) - response = client.faraday.get( - "/v2/transfers/#{transfer_id}/files/#{@id}/upload-url/#{part_number}", - {}, - client.auth_headers.merge('Content-Type' => 'application/json') - ) - client.ensure_ok_status!(response) - JSON.parse(response.body, symbolize_names: true).fetch(:url) - end - - def request_board_upload_url(client:, board_id:, part_number:) - response = client.faraday.get( - "/v2/boards/#{board_id}/files/#{@id}/upload-url/#{part_number}/#{@multipart.id}", - {}, - client.auth_headers.merge('Content-Type' => 'application/json') - ) - client.ensure_ok_status!(response) - JSON.parse(response.body, symbolize_names: true).fetch(:url) - end - - def complete_transfer_file(client:, transfer_id:) - body = {part_numbers: @multipart.part_numbers} - response = client.faraday.put( - "/v2/transfers/#{transfer_id}/files/#{@id}/upload-complete", - JSON.pretty_generate(body), - client.auth_headers.merge('Content-Type' => 'application/json') - ) - client.ensure_ok_status!(response) - JSON.parse(response.body, symbolize_names: true) - end - - def complete_board_file(client:, board_id:) - response = client.faraday.put( - "/v2/boards/#{board_id}/files/#{@id}/upload-complete", - '{}', - client.auth_headers.merge('Content-Type' => 'application/json') - ) - client.ensure_ok_status!(response) - JSON.parse(response.body, symbolize_names: true) - end -end diff --git a/lib/we_transfer_client/remote_link.rb b/lib/we_transfer_client/remote_link.rb deleted file mode 100644 index 48a82bc..0000000 --- a/lib/we_transfer_client/remote_link.rb +++ /dev/null @@ -1,9 +0,0 @@ -class RemoteLink - attr_reader :type - def initialize(id:, url:, type:, meta:) - @id = id - @url = url - @title = meta.fetch(:title) - @type = type - end -end diff --git a/lib/we_transfer_client/remote_transfer.rb b/lib/we_transfer_client/remote_transfer.rb deleted file mode 100644 index b268bd2..0000000 --- a/lib/we_transfer_client/remote_transfer.rb +++ /dev/null @@ -1,25 +0,0 @@ -class RemoteTransfer - attr_reader :files, :url, :state, :id - - def initialize(id:, state:, url:, message:, files: [], **) - @id = id - @state = state - @message = message - @url = url - @files = files_to_class(files) - end - - def prepare_file_upload(client:, file:, part_number:) - url = file.request_transfer_upload_url(client: client, transfer_id: @id, part_number: part_number) - chunk_size = file.multipart.chunk_size - [url, chunk_size] - end - - def prepare_file_completion(client:, file:) - file.complete_transfer_file(client: client, transfer_id: @id) - end - - def files_to_class(files) - files.map { |x| RemoteFile.new(x) } - end -end diff --git a/lib/we_transfer_client/transfer_builder.rb b/lib/we_transfer_client/transfer_builder.rb deleted file mode 100644 index 6bc5080..0000000 --- a/lib/we_transfer_client/transfer_builder.rb +++ /dev/null @@ -1,27 +0,0 @@ -class TransferBuilder - attr_reader :files - class TransferIOError < StandardError; end - - def initialize - @files = [] - end - - def add_file(name:, io:) - ensure_io_compliant!(io) - @files << FutureFile.new(name: name, io: io) - end - - def add_file_at(path:) - add_file(name: File.basename(path), io: File.open(path, 'rb')) - end - - def ensure_io_compliant!(io) - io.seek(0) - io.read(1) # Will cause things like Errno::EACCESS to happen early, before the upload begins - io.seek(0) # Also rewinds the IO for later uploading action - size = io.size # Will cause a NoMethodError - raise TransferIOError, 'The IO object given to add_file has a size of 0' if size <= 0 - rescue NoMethodError - raise TransferIOError, "The IO object given to add_file must respond to seek(), read() and size(), but #{io.inspect} did not" - end -end diff --git a/lib/we_transfer_client/transfers.rb b/lib/we_transfer_client/transfers.rb deleted file mode 100644 index 662e357..0000000 --- a/lib/we_transfer_client/transfers.rb +++ /dev/null @@ -1,73 +0,0 @@ -module WeTransfer - class Client - module Transfers - def create_transfer_and_upload_files(message:, &block) - future_transfer = create_future_transfer(message: message, &block) - remote_transfer = create_remote_transfer(future_transfer) - remote_transfer.files.each do |file| - check_for_file_duplicates(future_transfer.files, file) - local_file = future_transfer.files.select { |x| x.name == file.name }.first - upload_file(object: remote_transfer, file: file, io: local_file.io) - complete_file!(object: remote_transfer, file: file) - end - complete_transfer(transfer: remote_transfer) - end - - def get_transfer(transfer_id:) - request_transfer(transfer_id) - end - - private - - def create_transfer(message:, &block) - transfer = create_future_transfer(message: message, &block) - create_remote_transfer(transfer) - end - - def complete_transfer(transfer:) - complete_transfer_call(transfer) - end - - def create_future_transfer(message:, future_transfer_class: FutureTransfer, transfer_builder_class: TransferBuilder) - builder = transfer_builder_class.new - yield(builder) - future_transfer_class.new(message: message, files: builder.files) - rescue LocalJumpError - raise ArgumentError, 'No files were added to transfer' - end - - def create_remote_transfer(xfer) - authorize_if_no_bearer_token! - response = faraday.post( - '/v2/transfers', - JSON.pretty_generate(xfer.to_request_params), - auth_headers.merge('Content-Type' => 'application/json') - ) - ensure_ok_status!(response) - RemoteTransfer.new(JSON.parse(response.body, symbolize_names: true)) - end - - def complete_transfer_call(object) - authorize_if_no_bearer_token! - response = faraday.put( - "/v2/transfers/#{object.id}/finalize", - '', - auth_headers.merge('Content-Type' => 'application/json') - ) - ensure_ok_status!(response) - RemoteTransfer.new(JSON.parse(response.body, symbolize_names: true)) - end - - def request_transfer(transfer_id) - authorize_if_no_bearer_token! - response = faraday.get( - "/v2/transfers/#{transfer_id}", - {}, - auth_headers.merge('Content-Type' => 'application/json') - ) - ensure_ok_status!(response) - RemoteTransfer.new(JSON.parse(response.body, symbolize_names: true)) - end - end - end -end diff --git a/lib/we_transfer_client/version.rb b/lib/we_transfer_client/version.rb deleted file mode 100644 index 3bccd95..0000000 --- a/lib/we_transfer_client/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module WeTransfer - VERSION = '0.9.0.beta3' -end diff --git a/spec/board_integration_spec.rb b/spec/board_integration_spec.rb deleted file mode 100644 index c88cfea..0000000 --- a/spec/board_integration_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'spec_helper' - -describe WeTransfer::Client do - let(:small_file_name) { 'Japan-02.jpg' } - let(:big_file) { File.open(fixtures_dir + 'Japan-01.jpg', 'rb') } - let(:client) do - WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - end - - it 'creates a board with files and web items as a block' do - # Create a board with three items, one small file, one multipart file, and one web url - board = client.create_board_and_upload_items(name: 'Test Board', description: 'Test description') do |b| - b.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb')) - b.add_file(name: 'big file', io: big_file) - b.add_file_at(path: fixtures_dir + small_file_name) - b.add_web_url(url: 'http://www.wetransfer.com', title: 'WeTransfer Website') - end - - # the board url is set - expect(board.url =~ %r|https://we.tl/b-|).to be_truthy - - expect(board.items.map(&:class)).to eq([RemoteFile, RemoteFile, RemoteFile, RemoteLink]) - expect(board.items[1].multipart.part_numbers).to be > 1 - expect(board.items.count).to be(4) - - # TODO: make these experimental steps public - # # Add two new items to the board, one small file and one web url - # client.add_items(board: board) do |b| - # b.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb')) - # b.add_web_url(url: 'http://www.google.com', title: 'google') - # end - # expect(board.items.count).to be(6) - - # # Check if the board includes 3 File items and 2 Link items - # expect(board.items.select { |i| i.type == 'file' }.count).to be(4) - # expect(board.items.select { |i| i.type == 'link' }.count).to be(2) - - # # Upload the Files to the Board - # file_items = board.items.select { |i| i.type == 'file' } - # client.upload_file(object: board, file: file_items[0], io: File.open(__FILE__, 'rb')) - # client.upload_file(object: board, file: file_items[1], io: big_file) - # client.upload_file(object: board, file: file_items[2], io: File.open(fixtures_dir + small_file_name, 'rb')) - # client.upload_file(object: board, file: file_items[3], io: File.open(__FILE__, 'rb')) - - # # Complete all the files of the board - # file_items.each do |item| - # client.complete_file!(object: board, file: item) - # end - - # Do a get request to see if url is available - response = Faraday.get(board.url) - - # that should redirect us... - expect(response.status).to eq(302) - # ... to a board in the wetransfer domain - expect(response['location']).to start_with('https://boards.wetransfer') - - # Check for the Boards status to be downloadable - resulting_board = loop do - res = client.get_board(board: board) - break res if res.state != 'processing' - sleep 1 - end - - expect(resulting_board.state).to eq('downloadable') - expect(resulting_board.items.count).to be(4) - end -end diff --git a/spec/features/add_items_to_board_spec.rb b/spec/features/add_items_to_board_spec.rb deleted file mode 100644 index d435cf8..0000000 --- a/spec/features/add_items_to_board_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe WeTransfer::Client::Boards do - let(:client) { WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) } - let(:board) do - client.create_board(name: 'Test Board', description: 'Test Descritpion') - end - - describe '#add_items' do - before do - skip "this interface is still experimental" - end - - it 'adds items to a board' do - client.add_items(board: board) do |b| - b.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb')) - b.add_file_at(path: fixtures_dir + 'Japan-02.jpg') - b.add_web_url(url: 'http://www.google.com', title: 'google') - end - end - - it 'fails when no block is given' do - expect { - client.add_items(board: board) - }.to raise_error ArgumentError, /No items/ - end - - it 'fails when no board is passed as keyword argument' do - expect { - client.add_items do |b| - b.add_file_at(path: fixtures_dir + 'Japan-01.jpg') - end - }.to raise_error ArgumentError, /board/ - end - - it 'fails when file is not found' do - expect { - client.add_items(board: board) do |b| - b.add_file(name: 'file_not_found.rb', io: File.open('/path/to/non-existent-file.rb', 'r')) - end - }.to raise_error Errno::ENOENT, /No such file/ - end - - it 'fails when board is not a existing remote board' do - new_board = RemoteBoard.new(id: 123456, state: 'proccessing', url: 'https://www.we.tl/123456', name: 'fake board') - expect { - client.add_items(board: new_board) do |b| - b.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb')) - b.add_file_at(path: fixtures_dir + 'Japan-01.jpg') - b.add_web_url(url: 'http://www.google.com', title: 'google') - end - }.to raise_error WeTransfer::Client::Error, /404 code/ - end - end -end diff --git a/spec/features/client_creates_a_transfer_spec.rb b/spec/features/client_creates_a_transfer_spec.rb new file mode 100644 index 0000000..dc3e687 --- /dev/null +++ b/spec/features/client_creates_a_transfer_spec.rb @@ -0,0 +1,59 @@ +require "spec_helper" + +describe "transfer integration" do + around(:each) do |example| + WebMock.allow_net_connect! + example.run + WebMock.disable_net_connect! + end + + let(:client) { WeTransfer::Client.new(api_key: ENV.fetch("WT_API_KEY")) } + + it "Convenience method create_transfer_and_upload_files does it all!" do + transfer = client.create_transfer_and_upload_files(message: "test transfer") do |transfer| + transfer.add_file(name: "small_file_with_io", size: 10, io: StringIO.new("#" * 10)) + transfer.add_file(io: File.open('Gemfile')) + end + + # the transfer is (soon) ready to be downloaded. Time to inspect it: + + expect(transfer.files.map(&:name)) + .to match_array(%w[small_file_with_io Gemfile]) + expect(transfer.state) + .to eq "processing" + expect(transfer.url) + .to match %r|https://we.tl/t-| + end + + it "Create a client, a transfer, it uploads files and finalizes the transfer" do + transfer = client.create_transfer(message: "test transfer") do |transfer| + transfer.add_file(name: "small_file", size: 80) + transfer.add_file(name: "small_file_with_io", size: 10, io: StringIO.new("#" * 10)) + transfer.add_file(name: "multi_chunk_big_image.jpg", io: File.open('spec/fixtures/Japan-01.jpg')) + end + + expect(transfer.url.nil?) + .to eq true + + expect(transfer.id.nil?).to eq false + expect(transfer.state).to eq "uploading" + expect(transfer.files.any? { |f| f.id.nil? }).to eq false + + transfer.upload_file(name: "small_file", io: StringIO.new("#" * 80)) + transfer.complete_file(name: "small_file") + + transfer.upload_file(name: "small_file_with_io") + transfer.complete_file(name: "small_file_with_io") + + transfer.upload_file(name: "multi_chunk_big_image.jpg") + transfer.complete_file(name: "multi_chunk_big_image.jpg") + + transfer.finalize + + expect(transfer.state).to eq "processing" + + # Your transfer is available (after processing) at `transfer.url` + expect(transfer.url) + .to match %r|https://we.tl/t-| + end +end diff --git a/spec/features/create_board_spec.rb b/spec/features/create_board_spec.rb deleted file mode 100644 index 33e4f7a..0000000 --- a/spec/features/create_board_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'spec_helper' - -describe WeTransfer::Client::Boards do - let(:big_file) { File.open(fixtures_dir + 'Japan-01.jpg', 'r') } - let(:client) { WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) } - - describe '#create_board' do - before { - skip "this interface is still experimental" - } - - it 'creates a remote board' do - client.create_board(name: 'Test Board', description: 'Test Descritpion') - end - - it 'creates a board with items' do - client.create_board(name: 'Test Board', description: 'Test descrition') do |b| - b.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb')) - b.add_file(name: 'big file', io: big_file) - b.add_web_url(url: 'http://www.wetransfer.com', title: 'WeTransfer Website') - end - end - - it 'fails when name is missing' do - expect { - client.create_board(name: '', description: 'Test Descritpion') - }.to raise_error WeTransfer::Client::Error, /400 code/ - end - - it 'fails when file path is wrong' do - expect { - client.create_board(name: 'Test Board', description: 'Test descrition') do |b| - b.add_file(name: 'file_not_found.rb', io: File.open('path/to/non-existing-file.rb', 'r')) - end - }.to raise_error Errno::ENOENT, /No such file/ - end - - it 'fails when file name is missing' do - expect { - client.create_board(name: 'Test Board', description: 'Test descrition') do |b| - b.add_file(name: '', io: File.open(__FILE__, 'rb')) - end - }.to raise_error WeTransfer::Client::Error, /400 code/ - end - end -end diff --git a/spec/features/get_board_spec.rb b/spec/features/get_board_spec.rb deleted file mode 100644 index 0b71d36..0000000 --- a/spec/features/get_board_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe WeTransfer::Client::Boards do - let(:client) do - WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - end - - let(:board) do - client.create_board(name: 'Test Board', description: 'Test Descritpion') - end - - describe '#get_board' do - before do - skip "this interface is still experimental" - end - - it 'it gets a exisiting board' do - client.get_board(board: board) - end - - it 'fails when no board is given' do - expect { - client.get_board - }.to raise_error ArgumentError, /board/ - end - - it 'fails when board doenst exists' do - new_board = RemoteBoard.new(id: 123456, state: 'proccessing', url: 'https://www.we.tl/123456', name: 'fake board') - expect { - client.get_board(board: new_board) - }.to raise_error WeTransfer::Client::Error, /404 code/ - end - end -end diff --git a/spec/features/transfer_spec.rb b/spec/features/transfer_spec.rb deleted file mode 100644 index 09f0e4e..0000000 --- a/spec/features/transfer_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'spec_helper' - -describe WeTransfer::Client::Transfers do - let(:client) do - WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY'), logger: test_logger) - end - - let(:file_location) { fixtures_dir + 'Japan-02.jpg' } - - let(:created_transfer) do - client.create_transfer(message: 'Test transfer') do |builder| - builder.add_file(name: File.basename(file_location), io: File.open(file_location, 'rb')) - end - end - - describe "#create_transfer" do - before do - skip "this interface is still experimental" - end - - it "is needed to add a file" do - expect { client.create_transfer(message: "Transfer name") } - .to raise_error(ArgumentError, /^No files were added/) - end - - it "accepts a block to add files by their location" do - client.create_transfer(message: 'Test transfer') do |builder| - builder.add_file_at(path: file_location) - end - end - - it "accepts a block to add files by their io" do - client.create_transfer(message: 'Test transfer') do |builder| - builder.add_file(name: File.basename(file_location), io: File.open(file_location, 'rb')) - end - end - end - - describe "#complete_transfer" - describe "#get_transfer" -end diff --git a/spec/fixtures/Japan-02.jpg b/spec/fixtures/Japan-02.jpg deleted file mode 100644 index 01de240..0000000 Binary files a/spec/fixtures/Japan-02.jpg and /dev/null differ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f9c2660..e92f5b9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,29 +5,17 @@ add_filter '/spec/' end -require 'we_transfer_client' +require 'we_transfer' require 'pry' require 'rspec' require 'bundler' Bundler.setup require 'tempfile' require 'dotenv' +require 'webmock/rspec' Dotenv.load -module SpecHelpers - def fixtures_dir - __dir__ + '/fixtures/' - end - - def test_logger - Logger.new($stderr).tap { |log| log.level = Logger::WARN } - end -end - RSpec.configure do |config| - config.include SpecHelpers - config.extend SpecHelpers - config.expect_with :rspec do |c| c.syntax = :expect end @@ -37,3 +25,5 @@ def test_logger config.default_formatter = 'doc' config.order = :random end + +RSpec::Mocks.configuration.verify_partial_doubles = true diff --git a/spec/transfer_integration_spec.rb b/spec/transfer_integration_spec.rb deleted file mode 100644 index 9fd2b6c..0000000 --- a/spec/transfer_integration_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe WeTransfer::Client do - let(:client) { WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) } - let(:file_locations) { %w[Japan-01.jpg Japan-02.jpg] } - - describe described_class::Transfers do - pending 'creates a transfer with multiple files' do - skip "this interface is still experimental" - transfer = client.create_transfer(message: 'Japan: 🏯 & 🎎') do |builder| - file_locations.each do |file_location| - builder.add_file(name: File.basename(file_location), io: File.open(fixtures_dir + file_location, 'rb')) - end - end - - expect(transfer).to be_kind_of(RemoteTransfer) - - # it has an url that is not available (yet) - expect(transfer.url).to be(nil) - # it has no files (yet) - expect(transfer.files.first.url).to be(nil) - # it is in an uploading state - expect(transfer.state).to eq('uploading') - - # TODO: uncouple file_locations and transfer.files - file_locations.each_with_index do |location, index| - client.upload_file( - object: transfer, - file: transfer.files[index], - io: File.open(fixtures_dir + location, 'rb') - ) - client.complete_file!( - object: transfer, - file: transfer.files[index] - ) - end - - result = client.complete_transfer(transfer: transfer) - - # it has an url that is available - expect(result.url =~ %r|^https://we.tl/t-|).to be_truthy - - # it is in a processing state - expect(result.state).to eq('processing') - - response = Faraday.get(result.url) - # it hits the short-url with redirect - expect(response.status).to eq(302) - expect(response['location']).to start_with('https://wetransfer.com/') - end - end -end diff --git a/spec/we_transfer/client_spec.rb b/spec/we_transfer/client_spec.rb new file mode 100644 index 0000000..16bed24 --- /dev/null +++ b/spec/we_transfer/client_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe WeTransfer::Client do + subject(:client) { described_class.new(api_key: fake_key) } + let(:fake_key) { "fake key" } + let(:communicator) { instance_double(WeTransfer::Communicator) } + + it 'exposes VERSION' do + expect(WeTransfer::VERSION).to be_kind_of(String) + end + + before do + allow(WeTransfer::Communicator) + .to receive(:new) + .and_return(communicator) + end + + describe "initializer" do + it "needs an :api_key kw param" do + expect { described_class.new } + .to raise_error(ArgumentError, %r|api_key|) + + expect { described_class.new(api_key: 'fake key') } + .to_not raise_error + end + + it "initializes a WeTransfer::Communicator" do + expect(WeTransfer::Communicator) + .to receive(:new) + .with(fake_key) + + subject + end + + it "stores the Communicator instance in @communicator" do + allow(WeTransfer::Communicator) + .to receive(:new) + .and_return(communicator) + + expect(subject.instance_variable_get(:@communicator)) + .to eq communicator + end + end + + describe "#create_transfer" do + let(:transfer) { instance_double(WeTransfer::Transfer) } + + it "instantiates a Transfer with all needed arguments" do + allow(transfer) + .to receive(:persist) + + expect(WeTransfer::Transfer) + .to receive(:new) + .with( + message: 'fake transfer', + communicator: communicator + ) + .and_return(transfer) + + subject.create_transfer(message: 'fake transfer') + end + + it "accepts a block, that is passed to the persist method of the Transfer instance" do + allow(WeTransfer::Transfer) + .to receive(:new) + .and_return(transfer) + + expect(transfer) + .to receive(:persist) { |&transfer| transfer.call(name: 'test file', size: 8) } + + expect { |probe| subject.create_transfer(message: 'test transfer', &probe) } + .to yield_with_args(name: 'test file', size: 8) + end + end + + describe "#find_transfer" do + it "is delegated to communicator" do + transfer_id = 'fake-transfer-id' + + expect(communicator) + .to receive(:find_transfer) + .with(transfer_id) + + subject.find_transfer(transfer_id) + end + end +end diff --git a/spec/we_transfer/communicator_spec.rb b/spec/we_transfer/communicator_spec.rb new file mode 100644 index 0000000..0b61db2 --- /dev/null +++ b/spec/we_transfer/communicator_spec.rb @@ -0,0 +1,392 @@ +require 'spec_helper' + +describe WeTransfer::Communicator do + subject(:communicator) { described_class.new('fake api key') } + + let(:fake_transfer_id) { "fake-transfer-id" } + let(:fake_file_id) { "fake-file-id" } + + let!(:authentication_stub) { + stub_request(:post, described_class::API_URL_BASE + described_class::AUTHORIZE_URI) + .to_return(status: 200, body: { token: "fake-test-token" }.to_json, headers: {}) + } + + # In this test we use the call to #find_transfer. This is NOT exemplary, it serves only + # the purpose of doing a request/response cycles to handle specific status codes (2xx, 4xx, 50x) + describe ".ensure_ok_status!" do + context "on a successful request" do + let!(:find_transfer_stub) do + stub_request(:get, "#{WeTransfer::Communicator::API_URL_BASE}/v2/transfers/fake-transfer-id") + .to_return( + status: 200, + body: { + id: "24cd3f4ccf15232e5660052a3688c03f20190221200022", + state: "uploading", + message: "test transfer", + url: nil, + files: [ + { + id: "fake_file_id", + name: "test file", + size: 8, + multipart: { + part_numbers: 1, + chunk_size: 8 + }, + type: "file", + } + ], + expires_at: "2019-02-28T20:00:22Z", + }.to_json, + headers: {}, + ) + end + + it "does not raise a CommunicationError" do + subject.find_transfer('fake-transfer-id') + expect { subject.find_transfer('fake-transfer-id') } + .to_not raise_error + end + end + + context "on a client error" do + let!(:find_transfer_stub) do + stub_request(:get, "#{WeTransfer::Communicator::API_URL_BASE}/v2/transfers/fake-transfer-id"). + to_return( + status: 405, + body: '{ + "success":false, + "message":"fake message" + }', + headers: {}, + ) + end + + it "raises a CommunicationError" do + expect { subject.find_transfer('fake-transfer-id') } + .to raise_error(WeTransfer::CommunicationError) + end + + it "showing the error from the API to the user" do + expect { subject.find_transfer('fake-transfer-id') } + .to raise_error(%r|Response had a \d\d\d status code, message was 'fake message'\.|) + end + end + + context "on a server error" do + let!(:find_transfer_stub) do + stub_request(:get, "#{WeTransfer::Communicator::API_URL_BASE}/v2/transfers/fake-transfer-id"). + to_return( + status: 501, + body: '', + headers: {}, + ) + end + + it "raises a CommunicationError" do + expect { subject.find_transfer('fake-transfer-id') } + .to raise_error(WeTransfer::CommunicationError) + end + + it "is telling we can try again" do + expect { subject.find_transfer('fake-transfer-id') } + .to raise_error(%r|had a 501 status code.*could retry|) + end + + context "everything else" do + let(:find_transfer_stub) do + stub_request(:get, "#{WeTransfer::Communicator::API_URL_BASE}/v2/transfers/fake-transfer-id"). + to_return( + status: 302, + body: '', + headers: {}, + ) + end + + it "raises a CommunicationError" do + expect { subject.find_transfer('fake-transfer-id') } + .to raise_error(WeTransfer::CommunicationError) + end + + it "includes the error code in the message" do + expect { subject.find_transfer('fake-transfer-id') } + .to raise_error(%r|had a 302 status code|) + end + + it "informs the user we have no way how to continue" do + expect { subject.find_transfer('fake-transfer-id') } + .to raise_error(%r|no idea what to do with that|) + end + end + end + end + + describe "#find_transfer" do + let(:fake_transfer_id) { "fake-transfer-id" } + + let(:transfer_body) do + { + id: fake_transfer_id, + state: "uploading", + message: "test transfer", + url: nil, + files: files_stub, + expires_at: "2019-02-28T20:00:22Z" + } + end + + let(:files_stub) do + [ + { + id: "fake_file_id", + name: "test file", + size: 8, + multipart: { + part_numbers: 1, + chunk_size: 8 + }, + type: "file", + } + ] + end + + let!(:find_transfer_stub) do + stub_request( + :get, + described_class::API_URL_BASE + (described_class::TRANSFER_URI % fake_transfer_id) + ) + .to_return( + status: 200, + body: transfer_body.to_json, + headers: {}, + ) + end + + it "needs a transfer_id param" do + expect { communicator.find_transfer } + .to raise_error(ArgumentError) + end + + it "sends a GET to TRANSFER_URI" do + allow(WeTransfer::Transfer) + .to receive(:new) + .and_return(instance_double(WeTransfer::Transfer)) + + allow(communicator) + .to receive(:setup_transfer) + + communicator.find_transfer('fake-transfer-id') + + expect(find_transfer_stub) + .to have_been_requested + end + + it "instantiates a WeTransfer::Transfer" do + expect(WeTransfer::Transfer) + .to receive(:new) + .with(message: "test transfer", communicator: communicator) + + allow(communicator) + .to receive(:setup_transfer) + + communicator.find_transfer('fake-transfer-id') + end + + it "instantiates the Transfer with all expected attributes and values" do + expected = { + files: [ + { + id: "fake_file_id", + multipart: { + chunk_size: 8, + chunks: 1 + }, + name: "test file", + size: 8 + } + ], + id: "fake-transfer-id", + message: "test transfer", + state: "uploading", + url: nil + } + + expect(communicator.find_transfer('fake-transfer-id').to_h) + .to eq expected + end + end + + describe "#upload_url_for_chunk" do + let(:chunk) { 1 } + let(:fake_upload_url) { "https://fake.upload.url/123" } + + let!(:upload_url_stub) do + stub_request( + :get, + described_class::API_URL_BASE + ( + described_class::UPLOAD_URL_URI % [fake_transfer_id, fake_file_id, chunk] + ) + ) + .to_return( + status: 200, + body: { success: true, url: fake_upload_url }.to_json, + headers: {}, + ) + end + + it "should be invoked with arguments for transfer_id, file_id and chunk" do + expect { communicator.upload_url_for_chunk(fake_transfer_id, fake_file_id, chunk) } + .not_to raise_error + end + + it "returns the url from the response of the WeTransfer Public API" do + expect(communicator.upload_url_for_chunk(fake_transfer_id, fake_file_id, chunk)) + .to eq fake_upload_url + end + end + + describe "#persist_transfer" do + let!(:persist_transfer_stub) do + stub_request(:post, described_class::API_URL_BASE + described_class::TRANSFERS_URI) + .to_return( + status: 200, + body: { + success: true, + id: fake_transfer_id, + state: persisted_transfer_state, + message: "test transfer", + url: nil, + files: [ + { + id: fake_file_id, + name: "test file", + size: 8, + multipart: { + part_numbers: 1, + chunk_size: 8 + }, + type: "file" + } + ], + expires_at: "2019-03-13T16:14:02Z" + }.to_json, + headers: {}, + ) + end + + let(:transfer) do + WeTransfer::Transfer.new( + message: 'fake transfer', + communicator: communicator + ).add_file(name: 'test file', size: 8) + end + + let(:persisted_transfer_state) { "fake transfer state" } + + it "is invoked with a Transfer instance as argument" do + # allow(transfer) + # .to receive(:as_persist_params) + # .and_return(id: fake_transfer_id) + + # allow(transfer) + # .to receive(:find_file_by_name) + # .and_return(id: fake_file_id) + + expect { communicator.persist_transfer(transfer) } + .not_to raise_error + end + + it "invokes :as_persist_params on the transfer" do + # allow(transfer) + # .to receive(:find_file_by_name) + # .and_return(id: fake_file_id) + + expect(transfer) + .to receive(:as_persist_params) + .and_return(id: fake_transfer_id) + + communicator.persist_transfer(transfer) + end + + it "assigns the transfer an @id, @state and @url" do + expect(transfer) + .to receive(:instance_variable_set) + .with("@id", fake_transfer_id) + + expect(transfer) + .to receive(:instance_variable_set) + .with("@state", persisted_transfer_state) + + expect(transfer) + .to receive(:instance_variable_set) + .with("@url", nil) + + communicator.persist_transfer(transfer) + end + end + + describe "#finalize_transfer" do + let(:transfer_url) { "https://ready.for/download" } + let(:finalized_transfer_state) { "fake finalized state" } + + let!(:finalize_transfer_stub) do + stub_request(:put, described_class::API_URL_BASE + (described_class::FINALIZE_URI % fake_transfer_id)) + .to_return( + status: 200, + body: { + success: true, + id: fake_transfer_id, + state: finalized_transfer_state, + message: "test transfer", + url: transfer_url, + files: [ + { + id: fake_file_id, + name: "test file", + size: 8, + multipart: { + part_numbers: 1, + chunk_size: 8 + }, + type: "file" + } + ], + expires_at: "2019-03-13T16:14:02Z" + }.to_json, + headers: {}, + ) + end + + let(:transfer) do + WeTransfer::Transfer.new( + message: 'fake transfer', + communicator: communicator + ).add_file(name: 'test file', size: 8) + end + + before do + allow(transfer) + .to receive(:id) + .and_return(fake_transfer_id) + end + + it "is invoked with a Transfer instance" do + expect { communicator.finalize_transfer(transfer) } + .not_to raise_error + end + + it "assigns the transfer an @url" do + allow(transfer) + .to receive(:instance_variable_set) + + expect(transfer) + .to receive(:instance_variable_set) + .with("@url", transfer_url) + + communicator.finalize_transfer(transfer) + end + end + describe "#remote_transfer_params" + describe "#upload_chunk" + describe "#complete_file" +end diff --git a/spec/we_transfer/mini_io_spec.rb b/spec/we_transfer/mini_io_spec.rb new file mode 100644 index 0000000..1cca5a8 --- /dev/null +++ b/spec/we_transfer/mini_io_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe WeTransfer::MiniIO do + let(:io_spy) do + spy = instance_double(File) + %i[read rewind seek size].each do |acceptable_method| + allow(spy) + .to receive(acceptable_method) + end + + spy + end + + subject(:mini_io) { described_class.new(io_spy) } + + context "initializer" do + let(:not_an_io) { "Not an IO" } + let(:already_wrapped) { described_class.new(File.open('Gemfile')) } + + it "does not wrap the IO twice" do + expect(described_class.new(already_wrapped)) + .to eq already_wrapped + end + + %i[read rewind seek size].each do |required_method| + it "raises if IO doesn't respond to #{required_method}" do + expect { described_class.new(not_an_io) } + .to raise_error ArgumentError, %r|#{required_method}| + end + end + + it "returns a NullMiniIO if called with nil" do + expect described_class.new(nil).is_a?(WeTransfer::NullMiniIO) + end + end + + context "exposes specific methods on io" do + it "#read" do + expect(io_spy) + .to receive(:read) + .with(:foo) + + mini_io.read(:foo) + end + + it "#rewind" do + expect(io_spy) + .to receive(:rewind) + .with(no_args) + + mini_io.rewind + end + + it "#seek" do + expect(io_spy) + .to receive(:seek) + .with(:bar) + + mini_io.seek(:bar) + end + + it "#size" do + expect(io_spy) + .to receive(:size) + .with(no_args) + + mini_io.size + end + end + + describe "#name" do + it "guesses a name using File.basename" do + expect(File) + .to receive(:basename) + .with(io_spy) + + mini_io.name + end + end +end diff --git a/spec/we_transfer/transfer_spec.rb b/spec/we_transfer/transfer_spec.rb new file mode 100644 index 0000000..27c52bb --- /dev/null +++ b/spec/we_transfer/transfer_spec.rb @@ -0,0 +1,663 @@ +require "spec_helper" + +describe WeTransfer::Transfer do + let(:communicator) { instance_double(WeTransfer::Communicator) } + + describe ".create" do + it "instantiates a transfer" do + expect(described_class) + .to receive(:new) + .with(message: "test transfer", communicator: communicator) + .and_call_original + + begin + described_class.create( + message: "test transfer", + communicator: communicator + ) + rescue WeTransfer::Transfer::NoFilesAddedError + # We're rescuing here: This tests just that :create calls the initializer. + end + end + + it "calls #persist on the instantiated transfer" do + add_file_lambda = ->(transfer) { transfer.add_file(name: "test file", size: 8) } + + allow(communicator) + .to receive(:persist_transfer) + + expect_any_instance_of(described_class) + .to receive(:persist) do |*_args, &block| + expect(add_file_lambda).to be(block) + end + .and_call_original + + described_class.create( + message: "fake transfer", + communicator: communicator, + &add_file_lambda + ) + end + end + + describe ".find" do + before { skip("This interface is removed") } + + it "GETs the transfer by its id" do + described_class.find("fake-transfer-id") + + expect(find_transfer_stub) + .to have_been_requested + end + + it "sets up files" do + transfer = described_class.find("fake-transfer-id") + { + id: "fake_file_id", + name: "test file", + }.each do |attr, value| + expect(transfer.files.first.send(attr)).to eq value + end + end + end + + describe "initialize" do + it "needs a :message and a :communicator kw argument" do + expect { + described_class.new( + message: "test transfer", + communicator: communicator + ) + } + .to_not raise_error + end + + it "fails without a :message" do + expect { described_class.new(communicator: communicator) } + .to raise_error ArgumentError, %r|message| + end + + it "fails without a :communicator" do + expect { described_class.new(message: "test transfer") } + .to raise_error ArgumentError, %r|communicator| + end + end + + describe "#files" do + it "is a getter for @files" do + transfer = described_class.new( + message: "test transfer", + communicator: communicator + ) + + transfer.instance_variable_set :@files, "this is a fake" + + expect(transfer.files).to eq "this is a fake" + end + end + + describe "#persist" do + subject(:transfer) { + described_class.new( + message: "test transfer", + communicator: communicator + ) + } + + context "adding multiple files, using a block" do + let(:files_stub) { + [ + { + id: "fake_file_id", + name: "file1", + size: 8, + multipart: { + part_numbers: 1, + chunk_size: 8 + }, + type: "file", + }, { + id: "f740463d4e995720280fa08efe1911ef20190221200022", + name: "file2", + size: 8, + multipart: { + part_numbers: 1, + chunk_size: 8 + }, + type: "file", + } + ] + } + + it "works on a transfer without any files" do + # transfer = described_class.new(message: "test transfer") + + expect(communicator) + .to receive(:persist_transfer) + + transfer.persist do |transfer| + transfer.add_file(name: "file1", size: 8) + transfer.add_file(name: "file2", size: 8) + end + end + + it "can be used on a transfer that already has some files" do + # transfer = described_class.new(message: "test transfer") + transfer.add_file(name: "file1", size: 8) + + expect(communicator) + .to receive(:persist_transfer) + + transfer.persist { |transfer| transfer.add_file(name: "file2", size: 8) } + end + end + + context "not adding files, using a block" do + it "can create a remote transfer if the transfer already had one" do + # transfer = described_class.new(message: "test transfer") + transfer.add_file(name: "test file", size: 8) + + expect(communicator) + .to receive(:persist_transfer) + + transfer.persist + end + end + end + + describe "#add_file" do + subject(:transfer) do + described_class.new( + message: "test transfer", + communicator: communicator + ) + end + + let(:file_params) { { name: "test file", size: :bar, io: :baz } } + + it "can be called with only a name and a size" do + expect { transfer.add_file(name: "test file", size: :bar) } + .to_not raise_error + end + + context "when called with only an io" do + it "does work if the io provides a name and a size" do + transfer.add_file(io: File.open("Gemfile")) + end + + it "doesn't work if the io has no name" do + nameless_io = StringIO.new("I have no name, but a size I have") + expect { transfer.add_file(io: nameless_io) } + .to raise_error(ArgumentError, %r|name.*size.* or io should provide it|) + end + end + + it "can be called with an io, a name and a size" do + io = File.open("Gemfile") + expect { transfer.add_file(name: "Gemfile", size: 20, io: io) } + .to_not raise_error + end + + it "does not accept duplicate file names, case insensitive" do + file_a_params = { name: "same", size: 416 } + file_b_params = { name: "same", size: 501 } + file_c_params = { name: "SAME", size: 816 } + + transfer.add_file(file_a_params) + + expect { transfer.add_file(file_b_params) } + .to raise_error(WeTransfer::Transfer::DuplicateFileNameError) + + expect { transfer.add_file(file_c_params) } + .to raise_error(WeTransfer::Transfer::DuplicateFileNameError) + end + + context "WeTransferFile interaction" do + let(:fake_transfer_file) { instance_double(WeTransfer::WeTransferFile, name: "fake") } + + it "instantiates a TransferFile" do + expect(WeTransfer::WeTransferFile) + .to receive(:new) + .and_return(fake_transfer_file) + + transfer.add_file(file_params) + end + + it "adds the instantiated TransferFile to the @files collection" do + allow(WeTransfer::WeTransferFile) + .to receive(:new) + .and_return(fake_transfer_file) + + transfer.add_file(file_params) + + expect(subject.instance_variable_get(:@files)) + .to eq [fake_transfer_file] + end + + it "returns self" do + allow(WeTransfer::WeTransferFile) + .to receive(:new) + .and_return(fake_transfer_file) + + expect(transfer.add_file(file_params)).to eq transfer + end + end + end + + describe "#upload_file" do + subject(:transfer) do + described_class.new( + message: "test transfer", + communicator: communicator + ) + end + + it "should be called with :name" do + transfer.add_file(name: "test file", size: 8) + + expect { transfer.upload_file } + .to raise_error ArgumentError, %r|name| + end + + context "without io keyword param," do + context "if the WeTransferFile instance has an io" do + it "calls upload_chunk on the communicator" do + transfer.add_file(name: "test file", size: 8, io: StringIO.new("12345678")) + + # transfer.persist # does a network call + # fake-persist the transfer so we can spec the complete_file run + multipart = instance_double( + WeTransfer::RemoteFile::Multipart, + chunks: 1, + chunk_size: 20 + ) + transfer.instance_variable_set :@id, 'fake-transfer-id' + transfer.files.each do |f| + f.instance_variable_set :@id, 'fake-file-id' + f.instance_variable_set :@multipart, multipart + end + + allow(transfer) + .to receive(:upload_url_for_chunk) + .with(file_id: "fake-file-id", chunk: kind_of(Numeric)) + .and_return("https://fake.upload.url/123") + + expect(communicator) + .to receive(:upload_chunk) + .with("https://fake.upload.url/123", kind_of(StringIO)) + + transfer.upload_file(name: "test file") + end + + it "invokes :upload_url_for_chunk to obtain a PUT url" do + transfer.add_file(name: "test file", size: 8, io: StringIO.new("12345678")) + + # transfer.persist # does a network call + # fake-persist the transfer so we can spec the complete_file run + multipart = instance_double( + WeTransfer::RemoteFile::Multipart, + chunks: 1, + chunk_size: 20 + ) + transfer.instance_variable_set :@id, 'fake-transfer-id' + transfer.files.each do |f| + f.instance_variable_set :@id, 'fake-file-id' + f.instance_variable_set :@multipart, multipart + end + + allow(communicator) + .to receive(:upload_chunk) + + expect(transfer) + .to receive(:upload_url_for_chunk) + .with(file_id: "fake-file-id", chunk: 1) + + transfer.upload_file(name: "test file") + end + end + + it "breaks if the WeTransferFile instance does not have an io" do + transfer.add_file(name: "test file", size: 8) + + expect { transfer.upload_file(name: "test file") } + .to raise_error WeTransfer::RemoteFile::NoIoError, %r|'test file' cannot be uploaded| + end + end + + context "uploads each chunk" do + it "upload is triggered once if the io smaller than the server's chunk size" do + file_name = "test file" + contents = "12345678" + + transfer.add_file(name: file_name, io: StringIO.new(contents)) + + # transfer.persist # does a network call + # fake-persist the transfer so we can spec the complete_file run + multipart = instance_double( + WeTransfer::RemoteFile::Multipart, + chunks: 1, + chunk_size: 20 + ) + transfer.instance_variable_set :@id, 'fake-transfer-id' + transfer.files.each do |f| + f.instance_variable_set :@id, 'fake-file-id' + f.instance_variable_set :@multipart, multipart + end + + allow(communicator) + .to receive(:upload_chunk) + + expect(transfer) + .to receive(:upload_url_for_chunk) + .with(file_id: 'fake-file-id', chunk: 1) + .and_return("https://signed.url/123") + .once + + transfer.upload_file(name: file_name) + end + + it "upload is triggered twice if the io is has 2 chunks" do + file_name = "test file" + file_size = 1_000 + contents = "-" * file_size + + transfer.add_file(name: file_name, size: file_size, io: StringIO.new(contents)) + # transfer.persist # does a network call + # fake-persist the transfer so we can spec the complete_file run + multipart = instance_double( + WeTransfer::RemoteFile::Multipart, + chunks: 2, + chunk_size: 750 # not the right number, but we allow the server to set it + ) + transfer.instance_variable_set(:@id, 'fake-transfer-id') + + transfer.files.each do |f| + f.instance_variable_set :@id, 'fake-file-id' + f.instance_variable_set :@multipart, multipart + end + + expect(transfer) + .to receive(:upload_url_for_chunk) + .with(file_id: 'fake-file-id', chunk: kind_of(Numeric)) + .and_return("https://fake.upload.url/big-chunk-1", "https://fake.upload.url/big-chunk-2") + + expect(communicator) + .to receive(:upload_chunk) + .with(%r|https://fake.upload.url/big-chunk-\d|, kind_of(StringIO)) + .exactly(2).times + + transfer.upload_file(name: file_name) + end + end + end + + describe "#upload_files" do + subject(:transfer) do + described_class.new( + message: "test transfer", + communicator: communicator + ) + end + + let(:file_factory) { Struct.new(:name) } + + it "invokes :upload_file for each file in the files collection" do + transfer.instance_variable_set(:@files, 2.times.map { |n| file_factory.new("file-#{n}") }) + + expect(transfer) + .to receive(:upload_file) + .with(hash_including(:file, :name)) + .twice + + transfer.upload_files + end + end + + describe "#upload_url_for_chunk" do + subject(:transfer) do + described_class.new( + message: "test transfer", + communicator: communicator + ) + end + + it "must be invoked with a :chunk kw param" do + expect { transfer.upload_url_for_chunk } + .to raise_error(ArgumentError, %r|chunk|) + end + + it "must be invoked with either a :file_id or :name kw param" do + expect { transfer.upload_url_for_chunk(chunk: :foo) } + .to raise_error(ArgumentError, %r|name.*file_id|) + + allow(communicator) + .to receive(:upload_url_for_chunk) + + transfer.add_file(name: 'bar', size: 8) + transfer.files.first.instance_variable_set :@id, 'baz' + + expect { transfer.upload_url_for_chunk(chunk: :foo, name: 'bar') } + .not_to raise_error + + expect { transfer.upload_url_for_chunk(chunk: :foo, file_id: 'baz') } + .not_to raise_error + end + + it "invokes :upload_url_for_chunk on the communicator" do + allow(transfer) + .to receive(:id) + .and_return 'fake-transfer-id' + + file = Struct.new(:id).new('fake-file-id') + chunk = 3 + + expect(communicator) + .to receive(:upload_url_for_chunk) + .with(transfer.id, file.id, chunk) + + transfer.upload_url_for_chunk(file_id: file.id, chunk: chunk) + end + end + + describe "#complete_file" do + subject(:transfer) do + described_class.new( + message: "test transfer", + communicator: communicator + ) + end + + let(:file) { + instance_double( + WeTransfer::WeTransferFile, + id: 8, + name: 'meh', + multipart: multipart + ) + } + + let(:multipart) { + instance_double( + WeTransfer::RemoteFile::Multipart, + chunks: 1, + chunk_size: 20 + ) + } + + it "can be invoked with a :name kw param" do + transfer.add_file(name: 'meh', size: 8) + + # fake-persist the transfer so we can spec the complete_file run + transfer.instance_variable_set :@id, 'fake-transfer-id' + transfer.files.each do |f| + f.instance_variable_set :@id, 'fake-file-id' + f.instance_variable_set :@multipart, multipart + end + + expect(communicator) + .to receive(:complete_file) + + transfer.complete_file(name: 'meh') + end + + it "can be invoked with a :file kw param" do + allow(communicator) + .to receive(:complete_file) + + transfer.complete_file(file: file) + end + + it "if invoked with both a :name or a :file param, the :file is used" do + allow(communicator) + .to receive(:complete_file) + + expect(transfer) + .to_not receive(:find_file) + + transfer.complete_file(name: 'foo', file: file) + end + + it "needs to be invoked with either a :name or a :file kw param" do + expect { transfer.complete_file } + .to raise_error(ArgumentError, %r|name.*file|) + end + end + + describe "#complete_files" do + subject(:transfer) do + described_class.new( + message: "test transfer", + communicator: communicator + ) + end + + let(:file_factory) { Struct.new(:name) } + + it "invokes :complete_file for each file in the files collection" do + transfer.instance_variable_set(:@files, 2.times.map { |n| file_factory.new("file-#{n}") }) + + expect(transfer) + .to receive(:complete_file) + .with(hash_including(:file, :name)) + .twice + + transfer.complete_files + end + end + + describe "#finalize" do + subject(:transfer) do + WeTransfer::Transfer + .new( + message: "test transfer", + communicator: communicator, + ) + .add_file(name: "test file", size: 30) + end + + before { allow(transfer).to receive(:id).and_return("fake-transfer-id") } + + it "invokes :finalize_transfer on the communicator" do + expect(communicator) + .to receive(:finalize_transfer) + .with(transfer) + + transfer.finalize + end + end + + describe "#as_persist_params" do + subject(:transfer) do + described_class.new( + message: "test transfer", + communicator: communicator + ) + end + + it "returns a hash with the right values for :message and :files" do + expected = { message: "test transfer", files: [] } + expect(transfer.as_persist_params) + .to eq expected + end + + it "invokes :as_persist_params on each member of files" do + transfer.add_file(name: "test file 1", size: 8) + transfer.add_file(name: "test file 2", size: 8) + + transfer.files.each do |file| + expect(file) + .to receive(:as_persist_params) + end + transfer.as_persist_params + end + end + + describe "#to_h" do + let(:transfer) { described_class.new(message: 'test transfer', communicator: nil) } + let(:file_stub_1) { [instance_double(WeTransfer::WeTransferFile, name: 'foo', size: 8)] } + + it "has keys and values for id, state, url, message and files" do + allow(file_stub_1) + .to receive(:to_h) + .and_return('fake-file-to_h') + + allow(transfer) + .to receive(:files) + .and_return([file_stub_1]) + + allow(transfer) + .to receive(:id) + .and_return('fake-id') + + allow(transfer) + .to receive(:state) + .and_return('fake-state') + + allow(transfer) + .to receive(:url) + .and_return('fake-url') + + expected = { + id: "fake-id", + state: "fake-state", + url: "fake-url", + message: "test transfer", + files: ["fake-file-to_h"] + } + + expect(transfer.to_h) + .to match(expected) + end + + it "calls :to_h on all files" do + file_stub_2 = instance_double(WeTransfer::WeTransferFile, name: 'bar', size: 8) + + allow(transfer) + .to receive(:files) + .and_return([file_stub_1, file_stub_2]) + + expect(file_stub_1) + .to receive(:to_h) + + expect(file_stub_2) + .to receive(:to_h) + + transfer.to_h + end + end + + describe "#to_json" do + it "converts the results of #to_h" do + transfer = described_class.new(message: 'test transfer', communicator: nil) + + transfer_hash = { "foo" => "bar" } + + allow(transfer) + .to receive(:to_h) + .and_return(transfer_hash) + + expect(JSON.parse(transfer.to_json)) + .to eq(transfer_hash) + end + end +end diff --git a/spec/we_transfer/we_transfer_file_spec.rb b/spec/we_transfer/we_transfer_file_spec.rb new file mode 100644 index 0000000..408ffdf --- /dev/null +++ b/spec/we_transfer/we_transfer_file_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe WeTransfer::WeTransferFile do + describe "initializer" do + it "works with an IO that provides a name" + it "works with an IO and a name" + it "works with a name and a size" + + it "raises if name is unavailable" do + nameless_io = StringIO.new('I have no name, but a size I have') + expect { described_class.new(io: nameless_io) } + .to raise_error(ArgumentError) + + expect { described_class.new(size: 10) } + .to raise_error(ArgumentError) + end + + it "raises if size is unavailable" do + expect { described_class.new(name: 'README') } + .to raise_error(ArgumentError) + end + + it "infers the name using File.basename, with the io as its arg" do + io = File.open('Gemfile') + + expect(File) + .to receive(:basename) + .with(io) + .and_return "delegated" + + described_class.new(io: io) + end + + it "wraps the io in MiniIO" do + io = :foo + expect(WeTransfer::MiniIO) + .to receive(:new) + .with(io) + + described_class.new(io: io, name: 'test file', size: 8) + end + end + + describe "getters" do + subject { described_class.new(io: File.open('Gemfile')) } + + %i[name id io multipart].each do |getter| + it "has a getter for :#{getter}" do + expect { subject.send getter }.to_not raise_error + end + end + end + + describe "#as_persist_params" do + subject(:file) { described_class.new(name: 'test file', size: 8) } + + it "has key/values for name and size only" do + expect(file.as_persist_params.to_json) + .to eq %|{"name":"test file","size":8}| + end + end + + describe "#to_h" do + let(:file) { described_class.new(name: 'foo', size: 8) } + let(:multipart_stub) { instance_double(WeTransfer::RemoteFile::Multipart) } + + it "has keys and values for id, name, size and multipart" do + allow(file) + .to receive(:multipart) + .and_return(multipart_stub) + + allow(file) + .to receive(:id) + .and_return('fake-id') + + allow(multipart_stub) + .to receive(:to_h) + .and_return('fake-multipart') + + expected = { + id: 'fake-id', + multipart: 'fake-multipart', + size: 8, + name: 'foo' + + } + + expect(file.to_h) + .to match(expected) + end + + it "calls :to_h on multipart" do + allow(file) + .to receive(:multipart) + .and_return(multipart_stub) + + expect(multipart_stub) + .to receive(:to_h) + + file.to_h + end + end +end diff --git a/spec/we_transfer_client/board_builder_spec.rb b/spec/we_transfer_client/board_builder_spec.rb deleted file mode 100644 index be7d863..0000000 --- a/spec/we_transfer_client/board_builder_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'spec_helper' - -describe BoardBuilder do - describe '#initialize' do - it 'initializes with an empty array' do - expect(subject.items.empty?).to be(true) - end - end - - describe '#add_file' do - it 'returns an error when name is missing' do - expect { - subject.add_file(io: File.open(__FILE__, 'rb')) - }.to raise_error ArgumentError, /name/ - end - - it 'returns an error when io is missing' do - expect { - subject.add_file(name: 'file name') - }.to raise_error ArgumentError, /io/ - end - - it 'returns a error when file doesnt exists' do - expect { - subject.add_file(name: 'file name', io: File.open('foo', 'rb')) - }.to raise_error Errno::ENOENT - end - - it 'adds a file when name and io is given' do - subject.add_file(name: 'file name', io: File.open(__FILE__, 'rb')) - expect(subject.items.first).to be_kind_of(FutureFile) - end - end - - describe '#add_file_at' do - before do - skip "this interface is still experimental" - end - - it 'adds a file from a path' do - subject.add_file_at(path: __FILE__) - expect(subject.items.first).to be_kind_of(FutureFile) - end - - it 'throws a Error when file doesnt exists' do - expect { - subject.add_file_at(path: '/this/path/leads/to/nothing.exe') - }.to raise_error Errno::ENOENT - end - - it 'should call #add_file' do - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - client.create_board(name: 'Test board', description: 'A board description') do |b| - expect(b).to receive(:add_file).with(name: anything, io: kind_of(::IO)) - b.add_file_at(path: __FILE__) - end - end - end - - describe '#add_web_url' do - it 'adds a item to board when url and title are given' do - subject.add_web_url(url: 'http://www.wetransfer.com', title: 'wetransfer') - expect(subject.items.first).to be_kind_of(FutureLink) - end - end -end diff --git a/spec/we_transfer_client/boards_spec.rb b/spec/we_transfer_client/boards_spec.rb deleted file mode 100644 index 0e09440..0000000 --- a/spec/we_transfer_client/boards_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -require_relative '../../lib/we_transfer_client.rb' - -describe WeTransfer::Client::Boards do - describe '#create_board_and_upload_items' do - it 'creates a board and uploads the files' do - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - board = client.create_board_and_upload_items(name: 'test', description: 'test description') do |b| - b.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb')) - b.add_web_url(url: 'http://www.wetransfer.com', title: 'WeTransfer Website') - end - expect(board).to be_kind_of(RemoteBoard) - expect(board.url).to start_with('https://we.tl/') - expect(board.state).to eq('downloadable') - end - end - - describe "experimental features" do - before do - skip "this interface is still experimental" - end - - describe '#create_board' do - it 'creates a board' do - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - board = client.create_board(name: 'test', description: 'test description') - expect(board).to be_kind_of(RemoteBoard) - end - end - - describe "#add_items" do - it 'adds items to an existing board' do - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - board = client.create_board(name: 'test', description: 'test description') - updated_board = client.add_items(board: board) do |b| - b.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb')) - b.add_web_url(url: 'http://www.wetransfer.com', title: 'WeTransfer Website') - end - expect(updated_board).to be_kind_of(RemoteBoard) - expect(updated_board.items.size).to eq(2) - expect(updated_board.items.map(&:class)).to eq([RemoteFile, RemoteLink]) - end - end - - describe "#get_board" do - it 'gets board' do - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - board = client.create_board(name: 'test', description: 'test description') - board_request = client.get_board(board: board) - - expect(board_request).to be_kind_of(RemoteBoard) - end - end - end -end diff --git a/spec/we_transfer_client/future_board_spec.rb b/spec/we_transfer_client/future_board_spec.rb deleted file mode 100644 index d6371a3..0000000 --- a/spec/we_transfer_client/future_board_spec.rb +++ /dev/null @@ -1,98 +0,0 @@ -require 'spec_helper' - -describe FutureBoard do - let(:params) { { name: 'yes', description: 'A description about the board', items: [] } } - - describe '#initializer' do - it 'raises ArgumentError when no name is given' do - params.delete(:name) - expect { - described_class.new(params) - }.to raise_exception ArgumentError, /name/ - end - - it 'accepts a blank description' do - params.delete(:description) - described_class.new(params) - end - - it 'accepts a empty array as item argument' do - expect(described_class.new(params).items).to be_kind_of(Array) - end - end - - describe '#to_initial_request_params' do - it 'has a name' do - as_params = described_class.new(params).to_initial_request_params - expect(as_params[:name]).to be_kind_of(String) - end - - it 'has a description' do - as_params = described_class.new(params).to_initial_request_params - expect(as_params[:description]).to be(params[:description]) - end - end - - describe '#to_request_params' do - it 'has a name' do - as_params = described_class.new(params).to_request_params - expect(as_params[:name]).to be_kind_of(String) - end - - it 'has a description' do - as_params = described_class.new(params).to_request_params - expect(as_params[:description]).to be(params[:description]) - end - - it 'has items' do - file = FutureFile.new(name: 'yes', io: File.open(__FILE__, 'rb')) - params[:items] << file - as_params = described_class.new(params).to_request_params - expect(as_params[:items].count).to be(1) - end - end - - describe '#files' do - it 'returns only file items' do - file = FutureFile.new(name: 'yes', io: File.open(__FILE__, 'rb')) - link = FutureLink.new(url: 'https://www.wetransfer.com', title: 'WeTransfer') - future_board = described_class.new(params) - 3.times do - future_board.items << file - future_board.items << link - end - expect(future_board.items.size).to eq(6) - expect(future_board.files.size).to eq(3) - end - end - - describe '#links' do - it 'returns only link items' do - file = FutureFile.new(name: 'yes', io: File.open(__FILE__, 'rb')) - link = FutureLink.new(url: 'https://www.wetransfer.com', title: 'WeTransfer') - future_board = described_class.new(params) - 3.times do - future_board.items << file - future_board.items << link - end - expect(future_board.items.size).to eq(6) - expect(future_board.links.size).to eq(3) - end - end - - describe 'getters' do - let(:subject) { described_class.new(params) } - - it '#name' do - subject.name - end - - it '#description' do - subject.description - end - - it 'items' do - subject.items - end - end -end diff --git a/spec/we_transfer_client/future_file_spec.rb b/spec/we_transfer_client/future_file_spec.rb deleted file mode 100644 index 5bac7b5..0000000 --- a/spec/we_transfer_client/future_file_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe FutureFile do - let(:params) { { name: 'yes', io: File.open(__FILE__, 'rb') } } - - describe '#initilizer' do - it 'needs an :io keyword arg' do - params.delete(:io) - - expect { - described_class.new(params) - }.to raise_error(ArgumentError, /io/) - end - - it 'needs a :name keyword arg' do - params.delete(:name) - - expect { - described_class.new(params) - }.to raise_error(ArgumentError, /name/) - end - - it 'succeeds if given all arguments' do - described_class.new(params) - end - end - - describe '#to_request_params' do - it 'returns a hash with name and size' do - as_params = described_class.new(params).to_request_params - - expect(as_params[:name]).to eq('yes') - expect(as_params[:size]).to be_kind_of(Integer) - end - end - - describe 'getters' do - let(:subject) { described_class.new(params) } - - it '#name' do - subject.name - end - - it '#io' do - subject.io - end - end -end diff --git a/spec/we_transfer_client/future_link_spec.rb b/spec/we_transfer_client/future_link_spec.rb deleted file mode 100644 index 173fbe8..0000000 --- a/spec/we_transfer_client/future_link_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -describe FutureLink do - let(:params) { { url: 'http://www.wetransfer.com', title: 'WeTransfer' } } - - describe '#initializer' do - it 'needs a :url keyword arg' do - params.delete(:url) - expect { - described_class.new(params) - }.to raise_error(ArgumentError, /url/) - end - - it 'takes url when no title is given' do - params.delete(:title) - expect(described_class.new(params).title).to be(params.fetch(:url)) - end - - it 'succeeds if given all arguments' do - described_class.new(params) - end - end - - describe '#to_request_params' do - it 'creates params properly' do - as_params = described_class.new(params).to_request_params - - expect(as_params[:url]).to eq('http://www.wetransfer.com') - expect(as_params[:title]).to be_kind_of(String) - end - end - - describe 'getters' do - let(:subject) { described_class.new(params) } - - it '#url' do - subject.url - end - - it '#title' do - subject.title - end - end -end diff --git a/spec/we_transfer_client/future_transfer_spec.rb b/spec/we_transfer_client/future_transfer_spec.rb deleted file mode 100644 index 51d4437..0000000 --- a/spec/we_transfer_client/future_transfer_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -describe FutureTransfer do - let(:transfer) { described_class } - - describe '#initialize' do - it 'creates an empty array when initialized' do - expect(transfer.new(message: 'transfer').files).to be_kind_of(Array) - end - - it 'throws a error when message is not given' do - expect { - transfer.new - }.to raise_error ArgumentError - end - end - - describe 'getters' do - it 'message' do - transfer.new(message: 'test').message - end - - it 'files' do - transfer.new(message: 'test').files - end - end - - describe 'to_request_params' do - it 'includes the message' do - new_transfer = transfer.new(message: 'test') - expect(new_transfer.to_request_params[:message]).to be(new_transfer.message) - end - - it 'includes the files as an array' do - new_transfer = transfer.new(message: 'test') - expect(new_transfer.to_request_params[:files]).to eq([]) - end - end -end diff --git a/spec/we_transfer_client/remote_board_spec.rb b/spec/we_transfer_client/remote_board_spec.rb deleted file mode 100644 index 75ef24a..0000000 --- a/spec/we_transfer_client/remote_board_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ - -require 'spec_helper' - -describe RemoteBoard do - subject { described_class.new(params) } - - let(:params) { - { - id: SecureRandom.uuid, - state: 'downloadable', - url: 'http://wt.tl/123abcd', - name: 'RemoteBoard', - description: 'Test Description', - items: [ - { - id: 's7l1urvgqs1b6u9v720180911093825', - name: 'board_integration_spec.rb', - size: 3036, - multipart: { - part_numbers: 1, - chunk_size: 3036 - }, - type: 'file', - }, - { - id: 'storr6ua2l1fsl8lt20180911093826', - url: 'http://www.wetransfer.com', - meta: {title: 'WeTransfer Website'}, - type: 'link', - } - ] - } - } - - describe '#initializer' do - it 'is valid with all params' do - subject - end - - it 'is valid without description' do - params.delete(:description) - subject - end - - it 'is valid without items' do - params.delete(:items) - subject - end - - %i[id name state url].each do |param| - it "is invalid without #{param}" do - params.delete(param) - expect { - subject - }.to raise_error ArgumentError, %r[#{param}] - end - end - - describe 'items' do - it 'are instantiated' do - expect(subject.items.map(&:class)).to eq([RemoteFile, RemoteLink]) - end - - it 'raises ItemTypeError if the item has a wrong type' do - params[:items] = [{ type: 'foo' }] - expect { subject }.to raise_error(RemoteBoard::ItemTypeError) - end - end - end - - describe '#files' do - it 'returns only file item' do - expect(subject.items.size).to eq(2) - expect(subject.files.size).to eq(1) - end - end - - describe '#links' do - it 'returns only link item' do - expect(subject.items.size).to eq(2) - expect(subject.links.size).to eq(1) - end - end - - describe 'getters' do - %i[id items url state].each do |getter| - it "responds to ##{getter}" do - subject.send getter - end - end - end -end diff --git a/spec/we_transfer_client/remote_file_spec.rb b/spec/we_transfer_client/remote_file_spec.rb deleted file mode 100644 index d9c11be..0000000 --- a/spec/we_transfer_client/remote_file_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'spec_helper' - -describe RemoteFile do - let(:params) { - { - id: SecureRandom.uuid, - name: 'Board name', - size: Random.rand(9999999), - url: nil, - multipart: { - part_numbers: Random.rand(10), - id: SecureRandom.uuid, - chunk_size: RemoteBoard::CHUNK_SIZE, - }, - type: 'file', - }} - - describe '#initializer' do - it 'initialized when no url is given' do - params.delete(:url) - described_class.new(params) - end - - it 'initialized when no item is given' do - params.delete(:items) - described_class.new(params) - end - - it 'fails when id is missing' do - params.delete(:id) - expect { - described_class.new(params) - }.to raise_error ArgumentError, /id/ - end - - it 'fails when state is missing' do - params.delete(:size) - expect { - described_class.new(params) - }.to raise_error ArgumentError, /size/ - end - - it 'fails when url is missing' do - params.delete(:multipart) - expect { - described_class.new(params) - }.to raise_error ArgumentError, /multipart/ - end - - it 'fails when name is missing' do - params.delete(:name) - expect { - described_class.new(params) - }.to raise_error ArgumentError, /name/ - end - - it 'must have a struct in multipart' do - remote_file = described_class.new(params) - expect(remote_file.multipart).to be_kind_of(Struct) - end - - it 'multipart has partnumber, id and chunk_size keys' do - remote_file = described_class.new(params) - expect(remote_file.multipart.members).to eq(params[:multipart].keys) - end - end - - describe 'Getters' do - let(:subject) { described_class.new(params) } - - it '#multipart' do - subject.multipart - end - - it '#name' do - subject.name - end - - it '#type' do - subject.type - end - - it '#id' do - subject.id - end - - it '#url' do - subject.url - end - end -end diff --git a/spec/we_transfer_client/remote_link_spec.rb b/spec/we_transfer_client/remote_link_spec.rb deleted file mode 100644 index 883c6a5..0000000 --- a/spec/we_transfer_client/remote_link_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe RemoteLink do - let(:params) { - { - id: SecureRandom.uuid, - url: 'http://www.wetransfer.com', - meta: {title: 'wetransfer.com'}, - type: 'link', - } - } - - describe '#initialize' do - attributes = %i[id url type meta] - - attributes.each do |atttribute| - it "raises an ArgumentError when #{atttribute} is missing" do - params.delete(atttribute) - expect { - described_class.new(params) - }.to raise_error ArgumentError, %r{#{atttribute}} - end - end - end - - describe 'getters' do - subject { described_class.new(params) } - - it 'responds to #type' do - expect(subject.type).to eq 'link' - end - end -end diff --git a/spec/we_transfer_client/remote_transfer_spec.rb b/spec/we_transfer_client/remote_transfer_spec.rb deleted file mode 100644 index 7485660..0000000 --- a/spec/we_transfer_client/remote_transfer_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -require 'spec_helper' - -describe RemoteTransfer do - let(:params) { - { - id: '2ae97886522f375c1c6696799a56f0d820180912075119', - state: 'uploading', - message: 'Test transfer', - url: nil, - success: true, - files: - [ - { - id: '5e3823ea8ad54f259c85b776eaf7086e20180912075119', - name: 'transfer_integration_spec.rb', - size: 7361, - multipart: {part_numbers: 1, chunk_size: 7361}, - type: 'file', - }, - { - id: '43b5a6323102eced46f071f2db9ec2eb20180912075119', - name: 'two_chunks', - size: 6291460, - multipart: {part_numbers: 2, chunk_size: 5242880}, - type: 'file', - } - ] - } - } - - describe '#initialize' do - it 'fails when id is missing' do - params.delete(:id) - expect { - described_class.new(params) - }.to raise_error ArgumentError, /id/ - end - - it 'fails when state is missing' do - params.delete(:state) - expect { - described_class.new(params) - }.to raise_error ArgumentError, /state/ - end - - it 'fails when message is missing' do - params.delete(:message) - expect { - described_class.new(params) - }.to raise_error ArgumentError, /message/ - end - - it 'fails when files is a string' do - params.delete(:files) - params[:files] = 'Not an array' - expect { - described_class.new(params) - }.to raise_error NoMethodError - end - - it 'fails when files is a string' do - params.delete(:files) - params[:files] = 'Not an array' - expect { - described_class.new(params) - }.to raise_error NoMethodError - end - end - - describe '#files_to_class' do - it 'creates classes of remote files' do - transfer = described_class.new(params) - expect(transfer.files.map(&:class)).to eq([RemoteFile, RemoteFile]) - end - end - - describe '#prepare_file_upload' do - pending 'it retreives the upload url' do - fail - end - end - - describe '#Getters' do - subject { described_class.new(params) } - - it '#files' do - subject.files - end - - it '#url' do - subject.url - end - - it '#state' do - subject.state - end - - it '#id' do - subject.id - end - end -end diff --git a/spec/we_transfer_client/transfer_builder_spec.rb b/spec/we_transfer_client/transfer_builder_spec.rb deleted file mode 100644 index d511daa..0000000 --- a/spec/we_transfer_client/transfer_builder_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'spec_helper' - -describe TransferBuilder do - let(:transfer) { described_class.new } - - describe '#initialze' do - it 'initializes with an empty files array' do - expect(transfer.files.empty?).to be(true) - end - end - - describe '#add_file' do - it 'returns an error when name is missing' do - expect { - transfer.add_file(io: File.open(__FILE__, 'rb')) - }.to raise_error ArgumentError, /name/ - end - - it 'returns an error when io is missing' do - expect { - transfer.add_file(name: 'file name') - }.to raise_error ArgumentError, /io/ - end - - it 'returns a error when file doesnt exists' do - expect { - transfer.add_file(name: 'file name', io: File.open('foo', 'rb')) - }.to raise_error Errno::ENOENT - end - - it 'adds a file when name and io is given' do - transfer.add_file(name: 'file name', io: File.open(__FILE__, 'rb')) - expect(transfer.files.first).to be_kind_of(FutureFile) - end - end - - describe '#add_file_at' do - it 'adds a file from a path' do - transfer.add_file_at(path: __FILE__) - expect(transfer.files.first).to be_kind_of(FutureFile) - end - - it 'throws a Error when file doesnt exists' do - expect { - transfer.add_file_at(path: '/this/path/leads/to/nothing.exe') - }.to raise_error Errno::ENOENT - end - - pending 'should call #add_file' do - skip "Lets not trigger status:400 errors" - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY'), logger: test_logger) - client.create_transfer(message: 'A transfer message') do |builder| - expect(builder).to receive(:add_file).with(name: kind_of(String), io: kind_of(::File)) - builder.add_file_at(path: __FILE__) - end - end - end -end diff --git a/spec/we_transfer_client/transfers_spec.rb b/spec/we_transfer_client/transfers_spec.rb deleted file mode 100644 index 53c83e6..0000000 --- a/spec/we_transfer_client/transfers_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'spec_helper' - -describe WeTransfer::Client::Transfers do - describe '#create_transfer_and_upload_files' do - it 'creates a transfer and uploads the files' do - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - transfer = client.create_transfer_and_upload_files(message: 'test description') do |b| - b.add_file(name: File.basename(__FILE__), io: File.open(__FILE__, 'rb')) - b.add_file_at(path: fixtures_dir + 'Japan-01.jpg') - end - - expect(transfer).to be_kind_of(RemoteTransfer) - expect(transfer.url).to start_with('https://we.tl/') - - transfer = loop do - res = client.get_transfer(transfer_id: transfer.id) - break res if res.state != 'processing' - sleep 1 - end - - expect(transfer.state).to eq('downloadable') - end - - it 'fails when no files are added' do - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY')) - expect { - client.create_transfer_and_upload_files(message: 'test description') - }.to raise_error ArgumentError, /No files/ - end - - it 'fails with duplicate file names' do - client = WeTransfer::Client.new(api_key: ENV.fetch('WT_API_KEY', logger: test_logger)) - expect { - client.create_transfer_and_upload_files(message: 'All the (same) Things') do |builder| - builder.add_file(name: 'README.txt', io: StringIO.new("A thing")) - builder.add_file(name: 'README.txt', io: StringIO.new("another thing")) - end - }.to raise_error WeTransfer::Client::Error - end - end - - pending '#create_transfers' - pending "#complete_transfer" - pending "#get_transfer" -end diff --git a/spec/we_transfer_client_spec.rb b/spec/we_transfer_client_spec.rb index 1955098..e69de29 100644 --- a/spec/we_transfer_client_spec.rb +++ b/spec/we_transfer_client_spec.rb @@ -1,57 +0,0 @@ -require 'spec_helper' - -describe WeTransfer::Client do - subject { described_class.new(params) } - let(:params) { { api_key: ENV.fetch('WT_API_KEY') } } - - it 'exposes VERSION' do - expect(WeTransfer::VERSION).to be_kind_of(String) - end - - describe "#ensure_ok_status!" do - before(:all) { Response = Struct.new(:status) } - - context "on success" do - it "returns true if the status code is in the 2xx range" do - (200..299).each do |status_code| - response = Response.new(status_code) - expect(subject.ensure_ok_status!(response)).to be_truthy - end - end - end - - context "unsuccessful" do - it "raises with a message including the status code the server returned" do - response = Response.new("404") - expect { subject.ensure_ok_status!(response) } - .to raise_error(WeTransfer::Client::Error, %r/Response had a 404 code/) - - response = Response.new("Mehh") - expect { subject.ensure_ok_status!(response) } - .to raise_error(WeTransfer::Client::Error, %r/Response had a Mehh code/) - end - - it "if there is a server error, it raises with information that we can retry" do - (500..504).each do |status_code| - response = Response.new(status_code) - expect { subject.ensure_ok_status!(response) } - .to raise_error(WeTransfer::Client::Error, /we could retry/) - end - end - - it "on client error, it raises with information that the server cannot understand this" do - (400..499).each do |status_code| - response = Response.new(status_code) - expect { subject.ensure_ok_status!(response) } - .to raise_error(WeTransfer::Client::Error, /server will not accept this request even if retried/) - end - end - - it "if the status code is unknown, it raises a generic error" do - response = Response.new("I aint a status code") - expect { subject.ensure_ok_status!(response) } - .to raise_error(WeTransfer::Client::Error, /no idea what to do/) - end - end - end -end diff --git a/wetransfer.gemspec b/wetransfer.gemspec index 626ebaa..1f54df4 100644 --- a/wetransfer.gemspec +++ b/wetransfer.gemspec @@ -1,7 +1,7 @@ lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'we_transfer_client/version' +require 'we_transfer/version' Gem::Specification.new do |spec| spec.name = 'wetransfer' @@ -23,25 +23,23 @@ Gem::Specification.new do |spec| 'public gem pushes.' end + # TODO: Only add the needed files. see https://github.com/thoughtbot/shoulda-matchers/pull/1180 for inspiration spec.files = `git ls-files -z`.split("\x0").reject { |f| f =~ /^spec/ } - spec.bindir = 'exe' - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + # spec.bindir = 'exe' + # spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] spec.add_dependency 'faraday', '~> 0.12' + spec.add_dependency 'ks', '~> 0.0.2' - spec.add_development_dependency 'dotenv', '~> 2.2' spec.add_development_dependency 'bundler', '~> 1.16' + spec.add_development_dependency 'dotenv', '~> 2.0' + spec.add_development_dependency 'guard-rspec', '~> 4.7' + spec.add_development_dependency 'guard-rubocop', '~> 1.3' spec.add_development_dependency 'pry', '~> 0.11' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'simplecov', '~> 0.15' - spec.add_development_dependency 'wetransfer_style', '0.6.0' - spec.add_development_dependency 'guard-rspec', '~> 4.7' - spec.add_development_dependency 'guard-rubocop', '~> 1.3' + spec.add_development_dependency 'webmock' + spec.add_development_dependency 'wetransfer_style', '0.6.4' end - -# spec.add_development_dependency 'guard-flay' -# spec.add_development_dependency 'guard-flog' -# spec.add_development_dependency 'flay', '~> 2.4' -# spec.add_development_dependency 'flog'