From e3491259b79eeb0e7234b39816b15ecec9a3b55a Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Fri, 17 Jan 2025 12:21:35 +0000 Subject: [PATCH] Bug 39190: Rework (S)FTP into File::Transport classes This patch adds polymorphic transport classes to Koha for FTP and SFTP file transport configurations. We introduce a base class for interacting with the base configuration storage and subclasses to impliment the protocol specific transport handling. --- Koha/File/Transport.pm | 268 ++++++++++++++++++++++ Koha/File/Transport/FTP.pm | 183 +++++++++++++++ Koha/File/Transport/SFTP.pm | 142 ++++++++++++ Koha/File/Transports.pm | 65 ++++++ t/db_dependent/Koha/File/Transport.t | 173 ++++++++++++++ t/db_dependent/Koha/File/Transport/FTP.t | 93 ++++++++ t/db_dependent/Koha/File/Transport/SFTP.t | 94 ++++++++ 7 files changed, 1018 insertions(+) create mode 100644 Koha/File/Transport.pm create mode 100644 Koha/File/Transport/FTP.pm create mode 100644 Koha/File/Transport/SFTP.pm create mode 100644 Koha/File/Transports.pm create mode 100755 t/db_dependent/Koha/File/Transport.t create mode 100755 t/db_dependent/Koha/File/Transport/FTP.t create mode 100755 t/db_dependent/Koha/File/Transport/SFTP.t diff --git a/Koha/File/Transport.pm b/Koha/File/Transport.pm new file mode 100644 index 00000000000..7bbcf1e18f4 --- /dev/null +++ b/Koha/File/Transport.pm @@ -0,0 +1,268 @@ +package Koha::File::Transport; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use constant { + DEFAULT_TIMEOUT => 10, + TEST_FILE_NAME => '.koha_test_file', + TEST_FILE_CONTENT => "Hello, world!\n", + KEY_FILE_PERMISSIONS => '0600', +}; + +use Koha::Database; +use Koha::Exceptions::Object; +use Koha::Encryption; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::File::Transport - Base class for file transport handling + +=head1 DESCRIPTION + +Base class providing common functionality for FTP/SFTP file transport. + +=cut + +=head1 API + +=head2 Class methods + +=head3 store + + $server->store; + +Overloaded store method that ensures directory paths end with a forward slash. + +=cut + +sub store { + my ($self) = @_; + + # Encrypt sensitive data if changed + $self->_encrypt_sensitive_data(); + + # Normalize directory paths + for my $dir_field (qw(download_directory upload_directory)) { + my $dir = $self->$dir_field; + next unless $dir && $dir ne ''; + $self->$dir_field( $dir . '/' ) unless substr( $dir, -1 ) eq '/'; + } + + # Store + $self->SUPER::store; + + # Return the updated object including the encrypt_sensitive_data + return $self->discard_changes; +} + +=head3 plain_text_password + + my $password = $server->plain_text_password; + +Returns the decrypted plaintext password. + +=cut + +sub plain_text_password { + my ($self) = @_; + return unless $self->password; + return Koha::Encryption->new->decrypt_hex( $self->password ); +} + +=head3 plain_text_key + + my $key = $server->plain_text_key; + +Returns the decrypted plaintext key file. + +=cut + +sub plain_text_key { + my ($self) = @_; + return unless $self->key_file; + return Koha::Encryption->new->decrypt_hex( $self->key_file ) . "\n"; +} + +=head3 to_api + + my $json = $transport->to_api; + +Returns a JSON representation of the object suitable for API output, +excluding sensitive data. + +=cut + +sub to_api { + my ( $self, $params ) = @_; + + my $json = $self->SUPER::to_api($params) or return; + delete @{$json}{qw(password key_file)}; # Remove sensitive data + + return $json; +} + +=head3 to_api_mapping + +This method returns the mapping for representing a Koha::File::Transport object +on the API. + +=cut + +sub to_api_mapping { + return { id => 'sftp_server_id' }; +} + +=head3 test_connection + + $transport->test_connection + +Method to test the connection for the configuration of the current file server + +=cut + +sub test_connection { + my ($self) = @_; + + $self->connect or return; + + for my $dir_type (qw(download upload)) { + my $dir = $self->{"${dir_type}_directory"}; + next if $dir eq ''; + + $self->change_directory($dir) or return; + $self->list_files() or return; + } + + return 1; +} + +=head2 Subclass methods + +Interface methods that must be implemented by subclasses + +=head3 connect + + $transport->connect(); + +Method for connecting the current transport to the file server + +=cut + +sub connect { + my ($self) = @_; + die "Subclass must implement connect"; +} + +=head3 upload_file + + $transport->upload_file($file); + +Method for uploading a file to the current file server + +=cut + +sub upload_file { + my ( $self, $local_file, $remote_file ) = @_; + die "Subclass must implement upload_file"; +} + +=head3 download_file + + $transport->download_file($file); + +Method for downloading a file from the current file server + +=cut + +sub download_file { + my ( $self, $remote_file, $local_file ) = @_; + die "Subclass must implement download_file"; +} + +=head3 change_directory + + my $files = $transport->change_directory($path); + +Method for changing the current directory on the connected file server + +=cut + +sub change_directory { + my ( $self, $path ) = @_; + die "Subclass must implement change_directory"; +} + +=head3 list_files + + my $files = $transport->list_files($path); + +Method for listing files in the current directory of the connected file server + +=cut + +sub list_files { + my ( $self, $path ) = @_; + die "Subclass must implement list_files"; +} + +=head2 Internal methods + +=head3 _encrypt_sensitive_data + +Handle encryption of sensitive data + +=cut + +sub _encrypt_sensitive_data { + my ($self) = @_; + my $encryption = Koha::Encryption->new; + + # Only encrypt if the value has changed (is_changed from Koha::Object) + if ( ( !$self->in_storage || $self->is_changed('password') ) && $self->password ) { + $self->password( $encryption->encrypt_hex( $self->password ) ); + } + + if ( ( !$self->in_storage || $self->is_changed('key_file') ) && $self->key_file ) { + $self->key_file( $encryption->encrypt_hex( _dos2unix( $self->key_file ) ) ); + } +} + +=head3 _dos2unix + +Return a CR-free string from an input + +=cut + +sub _dos2unix { + my $dosStr = shift; + + return $dosStr =~ s/\015\012/\012/gr; +} + +=head3 _type + +Return type of Object relating to Schema Result + +=cut + +sub _type { + return 'SftpServer'; +} + +1; diff --git a/Koha/File/Transport/FTP.pm b/Koha/File/Transport/FTP.pm new file mode 100644 index 00000000000..ee0a1edea94 --- /dev/null +++ b/Koha/File/Transport/FTP.pm @@ -0,0 +1,183 @@ +package Koha::File::Transport::FTP; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Net::FTP; +use Try::Tiny; + +use base qw(Koha::File::Transport); + +=head1 NAME + +Koha::File::Transport::FTP - FTP implementation of file transport + +=head2 Class methods + +=head3 connect + + my $success = $self->connect; + +Start the FTP transport connect, returns true on success or undefined on failure. + +=cut + +sub connect { + my ($self) = @_; + + $self->{connection} = Net::FTP->new( + $self->host, + Port => $self->port, + Timeout => $self->DEFAULT_TIMEOUT, + Passive => $self->passive ? 1 : 0, + ) or return $self->_abort_operation(); + + $self->{connection}->login( $self->user_name, $self->plain_text_password ) + or return $self->_abort_operation(); + + $self->add_message( + { + message => "Connect succeeded", + type => 'success', + payload => { detail => '' } + } + ); + + return 1; +} + +=head3 upload_file + + my $success = $transport->upload_file($fh); + +Passed a filehandle, this will upload the file to the current directory of the server connection. + +Returns true on success or undefined on failure. + +=cut + +sub upload_file { + my ( $self, $local_file, $remote_file ) = @_; + + $self->{connection}->put( $local_file, $remote_file ) + or return $self->_abort_operation(); + + $self->add_message( + { + message => "Upload succeeded", + type => 'success', + payload => { detail => '' } + } + ); + + return 1; +} + +=head3 download_file + + my $success = $transport->download_file($filename); + +Passed a filename, this will download the file from the current directory of the server connection. + +Returns true on success or undefined on failure. + +=cut + +sub download_file { + my ( $self, $remote_file, $local_file ) = @_; + + $self->{connection}->get( $remote_file, $local_file ) + or return $self->_abort_operation(); + + $self->add_message( + { + message => "Download succeeded", + type => 'success', + payload => { detail => '' } + } + ); + + return 1; +} + +=head3 change_directory + + my $success = $server->change_directory($directory); + +Passed a directory name, this will change the current directory of the server connection. + +Returns true on success or undefined on failure. + +=cut + +sub change_directory { + my ( $self, $remote_directory ) = @_; + + $self->{connection}->cwd($remote_directory) or $self->_abort_operation(); + + $self->add_message( + { + message => "Changed directory succeeded", + type => 'success', + payload => { detail => '' } + } + ); + + return 1; +} + +=head3 list_files + + my @files = $server->list_files; + +Returns an array of filenames found in the current directory of the server connection. + +=cut + +sub list_files { + my ($self) = @_; + my $file_list = $self->{connection}->ls or return $self->_abort_operation(); + + $self->add_message( + { + message => "Listing files succeeded", + type => 'success', + payload => { detail => '' } + } + ); + + return $file_list; +} + +sub _abort_operation { + my ( $self, $message ) = @_; + + $self->add_message( + { + message => $self->{connection} ? $self->{connection}->message : $@, + type => 'error', + payload => { detail => $self->{connection} ? $self->{connection}->status : '' } + } + ); + + if ( $self->{connection} ) { + $self->{connection}->abort; + } + + return; +} + +1; diff --git a/Koha/File/Transport/SFTP.pm b/Koha/File/Transport/SFTP.pm new file mode 100644 index 00000000000..26efdd5df4e --- /dev/null +++ b/Koha/File/Transport/SFTP.pm @@ -0,0 +1,142 @@ +package Koha::File::Transport::SFTP; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Koha::Logger; + +use Net::SFTP::Foreign; +use Try::Tiny; + +use base qw(Koha::File::Transport); + +=head1 NAME + +Koha::File::Transport::SFTP - SFTP implementation of file transport + +=cut + +sub connect { + my ($self) = @_; + + $self->{connection} = Net::SFTP::Foreign->new( + host => $self->host, + user => $self->user_name, + password => $self->plain_text_password, + timeout => $self->DEFAULT_TIMEOUT, + more => [qw(-o StrictHostKeyChecking=no)], + ); + $self->{connection}->die_on_error("SFTP failure for remote host"); + + return $self->_abort_operation() if ( $self->{connection}->error ); + + $self->add_message( + { + message => $self->{connection}->status, + type => 'success', + payload => { detail => '' } + } + ); + + return 1; +} + +sub upload_file { + my ( $self, $local_file, $remote_file ) = @_; + + my $logger = Koha::Logger->get_logger(); + + $self->{connection}->put( $local_file, $remote_file ) or return $self->_abort_operation(); + + $self->add_message( + { + message => $self->{connection}->status, + type => 'success', + payload => { detail => '' } + } + ); + + return 1; +} + +sub download_file { + my ( $self, $remote_file, $local_file ) = @_; + + $self->{connection}->get( $remote_file, $local_file ) or return $self->_abort_operation(); + + $self->add_message( + { + message => $self->{connection}->status, + type => 'success', + payload => { detail => '' } + } + ); + + return 1; +} + +sub change_directory { + my ( $self, $remote_directory ) = @_; + + $self->{connection}->setcwd($remote_directory) or return $self->_abort_operation(); + + $self->add_message( + { + message => $self->{connection}->status, + type => 'success', + payload => { detail => '' } + } + ); + + return 1; +} + +sub list_files { + my ($self) = @_; + + my $file_list = $self->{connection}->ls or return $self->_abort_operation(); + + $self->add_message( + { + message => $self->{connection}->status, + type => 'success', + payload => { detail => '' } + } + ); + + return $file_list; +} + +sub _abort_operation { + my ($self) = @_; + + $self->add_message( + { + message => $self->{connection}->error, + type => 'error', + payload => { detail => $self->{connection} ? $self->{connection}->status : '' } + } + ); + + if ( $self->{connection} ) { + $self->{connection}->abort; + } + + return; +} + +1; diff --git a/Koha/File/Transports.pm b/Koha/File/Transports.pm new file mode 100644 index 00000000000..1c37ea76c59 --- /dev/null +++ b/Koha/File/Transports.pm @@ -0,0 +1,65 @@ +package Koha::File::Transports; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Koha::Database; +use Koha::Exceptions; + +use Koha::File::Transport::SFTP; +use Koha::File::Transport::FTP; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::File::Transports - Koha File Transports Object set class + +=head1 API + +=head2 Internal methods + +=head3 _type + +Return type of object, relating to Schema ResultSet + +=cut + +sub _type { + return 'SftpServer'; +} + +=head3 object_class + +Return object class dynamically based on transport + +=cut + +sub object_class { + my ( $self, $object ) = @_; + + return 'Koha::File::Transport' unless $object; + + my %class_map = ( + sftp => 'Koha::File::Transport::SFTP', + ftp => 'Koha::File::Transport::FTP', + ); + + return $class_map{ lc( $object->transport ) } || 'Koha::File::Transport'; +} + +1; diff --git a/t/db_dependent/Koha/File/Transport.t b/t/db_dependent/Koha/File/Transport.t new file mode 100755 index 00000000000..ce26900f0db --- /dev/null +++ b/t/db_dependent/Koha/File/Transport.t @@ -0,0 +1,173 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 4; +use Test::Exception; +use Test::Warn; + +use Koha::File::Transports; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +subtest 'store() tests' => sub { + plan tests => 6; + $schema->storage->txn_begin; + + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { + transport => 'ftp', + password => undef, + key_file => undef, + download_directory => undef, + upload_directory => undef + } + } + ); + + subtest 'Test store with empty directories' => sub { + plan tests => 2; + + $transport->set( { download_directory => '', upload_directory => '' } )->store(); + + is( $transport->download_directory, '', 'Empty download directory remains empty' ); + is( $transport->upload_directory, '', 'Empty upload directory remains empty' ); + }; + + subtest 'Test store with directories missing trailing slash' => sub { + plan tests => 2; + + $transport->set( { download_directory => '/tmp/download', upload_directory => '/tmp/upload' } )->store(); + + is( $transport->download_directory, '/tmp/download/', 'Added trailing slash to download directory' ); + is( $transport->upload_directory, '/tmp/upload/', 'Added trailing slash to upload directory' ); + }; + + subtest 'Test store with directories having trailing slash' => sub { + plan tests => 2; + + $transport->set( { download_directory => '/tmp/download/', upload_directory => '/tmp/upload/' } )->store(); + + is( $transport->download_directory, '/tmp/download/', 'Kept existing trailing slash in download directory' ); + is( $transport->upload_directory, '/tmp/upload/', 'Kept existing trailing slash in upload directory' ); + }; + + subtest 'Test store with mixed trailing slashes' => sub { + plan tests => 2; + + $transport->set( { download_directory => '/tmp/download', upload_directory => '/tmp/upload/' } )->store(); + + is( $transport->download_directory, '/tmp/download/', 'Added missing trailing slash to download directory' ); + is( $transport->upload_directory, '/tmp/upload/', 'Kept existing trailing slash in upload directory' ); + }; + + subtest 'Test store with undefined directories' => sub { + plan tests => 2; + + $transport->set( { download_directory => undef, upload_directory => undef } )->store(); + + is( $transport->download_directory, undef, 'Undefined download directory remains undefined' ); + is( $transport->upload_directory, undef, 'Undefined upload directory remains undefined' ); + }; + + subtest 'Test encryption of sensitive data' => sub { + plan tests => 2; + + $transport->set( { password => "test123", key_file => "test321" } )->store(); + + isnt( $transport->password, "test123", 'Password is encrypted on store' ); + isnt( $transport->upload_directory, "test321", 'Key file is encrypted on store' ); + + }; + + $schema->storage->txn_rollback; +}; + +subtest 'to_api() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $transport = $builder->build_object( { class => 'Koha::File::Transports', value => { status => undef } } ); + + ok( !exists $transport->to_api->{password}, 'Password is not part of the API representation' ); + ok( !exists $transport->to_api->{key_file}, 'Key file is not part of the API representation' ); + + $schema->storage->txn_rollback; +}; + +subtest 'plain_text_password() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { + transport => 'ftp', + password => undef, + key_file => undef, + } + } + ); + $transport->password('test123')->store(); + + my $transport_plain_text_password = $transport->plain_text_password; + + isnt( $transport_plain_text_password, $transport->password, 'Password and password hash shouldn\'t match' ); + is( $transport_plain_text_password, 'test123', 'Password should be in plain text' ); + + $schema->storage->txn_rollback; +}; + +subtest 'plain_text_key() tests' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { + transport => 'ftp', + password => undef, + key_file => undef + } + } + ); + $transport->key_file("test321")->store(); + + my $transport_plain_text_key = $transport->plain_text_key; + + isnt( $transport_plain_text_key, $transport->key_file, 'Key file and key file hash shouldn\'t match' ); + is( $transport_plain_text_key, "test321\n", 'Key file should be in plain text' ); + + $schema->storage->txn_rollback; +}; + +1; diff --git a/t/db_dependent/Koha/File/Transport/FTP.t b/t/db_dependent/Koha/File/Transport/FTP.t new file mode 100755 index 00000000000..c4f7fd00d89 --- /dev/null +++ b/t/db_dependent/Koha/File/Transport/FTP.t @@ -0,0 +1,93 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 5; +use Test::Exception; +use Test::Warn; + +use Koha::File::Transports; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +subtest 'connect() tests' => sub { + plan tests => 1; + + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'ftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'connect' ); +}; + +subtest 'upload_file() tests' => sub { + plan tests => 1; + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'ftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'upload_file' ); +}; + +subtest 'download_file() tests' => sub { + plan tests => 1; + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'ftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'download_file' ); +}; + +subtest 'change_directory() tests' => sub { + plan tests => 1; + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'ftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'change_directory' ); +}; + +subtest 'list_files() tests' => sub { + plan tests => 1; + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'ftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'list_files' ); +}; + +1; diff --git a/t/db_dependent/Koha/File/Transport/SFTP.t b/t/db_dependent/Koha/File/Transport/SFTP.t new file mode 100755 index 00000000000..d3293f4242e --- /dev/null +++ b/t/db_dependent/Koha/File/Transport/SFTP.t @@ -0,0 +1,94 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Test::More tests => 5; +use Test::Exception; +use Test::Warn; + +use Koha::File::Transports; + +use t::lib::TestBuilder; +use t::lib::Mocks; + +my $schema = Koha::Database->new->schema; +my $builder = t::lib::TestBuilder->new; + +subtest 'connect() tests' => sub { + plan tests => 2; + + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'sftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'connect' ); + dies_ok { $transport->connect } 'connect() should die without proper setup'; +}; + +subtest 'upload_file() tests' => sub { + plan tests => 1; + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'sftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'upload_file' ); +}; + +subtest 'download_file() tests' => sub { + plan tests => 1; + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'sftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'download_file' ); +}; + +subtest 'change_directory() tests' => sub { + plan tests => 1; + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'sftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'change_directory' ); +}; + +subtest 'list_files() tests' => sub { + plan tests => 1; + my $transport = $builder->build_object( + { + class => 'Koha::File::Transports', + value => { transport => 'sftp', password => 'testpass' } + } + ); + + can_ok( $transport, 'list_files' ); +}; + +1; -- 2.48.1