/*
*  psocksxx - A C++ wrapper for POSIX sockets
*  Copyright (C) 2013 Uditha Atukorala
*
*  This software library is free software; you can redistribute it and/or modify
*  it under the terms of the GNU Lesser General Public License as published by
*  the Free Software Foundation; either version 3 of the License, or
*  (at your option) any later version.
*
*  This software library 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 Lesser General Public License for more details.
*
*  You should have received a copy of the GNU Lesser General Public License
*  along with this software library. If not, see .
*
*/
#include "sockstreambuf_test.h"
#include "lecho.h"
#include 
#include 
#include 
#include 
#include 
// register the fixture into the 'registry'
CPPUNIT_TEST_SUITE_REGISTRATION( sockstreambuf_test );
// use namespace psocksxx
using namespace psocksxx;
sockstreambuf_test::sockstreambuf_test() { }
sockstreambuf_test::~sockstreambuf_test() { }
void sockstreambuf_test::connect_local() throw() {
	int csock;
	sockaddr_un saddr;
	const char * path = LOCAL_SOCK_PATH;
	bzero( (void *) &saddr, sizeof( saddr ) );
	saddr.sun_family = AF_LOCAL;
	strcpy( saddr.sun_path, path );
	// not worth having error handling here so we hope for the best
	csock = socket( AF_LOCAL, SOCK_STREAM, 0 );
	connect( csock, (::sockaddr *) &saddr, sizeof( sockaddr_un ) );
}
void sockstreambuf_test::setUp() {
	// initialise locals
	_sockaddr.sa_family = AF_LOCAL;
}
void sockstreambuf_test::tearDown() {
}
void sockstreambuf_test::test_constructors() {
	const sockstreambuf ssb;
	const sockstreambuf ssb_s( -1 );
	// check the default constructor
	CPPUNIT_ASSERT( ssb.socket() == ssb_s.socket() );
}
void sockstreambuf_test::test_open_close_local_ip() {
	sockstreambuf ssb;
	CPPUNIT_ASSERT_NO_THROW_MESSAGE( "Failed to open socket communication end-point",
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::ipproto_ip ) );
	// close the opened socket end-point
	ssb.close();
}
void sockstreambuf_test::test_flush_empty() {
	sockstreambuf ssb( -1 );
	CPPUNIT_ASSERT( sockstreambuf::eof == ssb.flush() );
}
void sockstreambuf_test::test_bad_flush() {
	// socket stream buffer
	sockstreambuf ssb( -1 );
	// add a char into the buffer
	ssb.sputc( 'c' );
	// flush buffer
	CPPUNIT_ASSERT( sockstreambuf::eof == ssb.flush() );
}
void sockstreambuf_test::test_bad_connect_failure() {
	// socket stream buffer
	sockstreambuf ssb( -1 );
	// this should throw a bad file descriptor error
	CPPUNIT_ASSERT_THROW( ssb.connect( &_sockaddr ), sockexception );
}
void sockstreambuf_test::test_bad_bind_failure() {
	// socket stream buffer
	sockstreambuf ssb( -1 );
	// this should throw a bad file descriptor error
	CPPUNIT_ASSERT_THROW( ssb.bind( &_sockaddr ), sockexception );
}
void sockstreambuf_test::test_bad_listen_failure() {
	// socket stream buffer
	sockstreambuf ssb( -1 );
	// this should throw a bad file descriptor error
	CPPUNIT_ASSERT_THROW( ssb.listen(), sockexception );
}
void sockstreambuf_test::test_bad_accept_failure() {
	// socket stream buffer
	sockstreambuf ssb( -1 );
	// this should throw a bad file descriptor error
	CPPUNIT_ASSERT_THROW( ssb.accept(), sockexception );
}
void sockstreambuf_test::test_local_bind() {
	// socket stream buffer
	sockstreambuf ssb;
	// local (unix) socket address
	const char * path = LOCAL_SOCK_PATH;
	lsockaddr saddr( path );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// bind to address
	CPPUNIT_ASSERT_NO_THROW( ssb.bind( &saddr ) );
	// close socket
	ssb.close();
	// unlink
	if ( unlink( path ) !=0 ) {
		CPPUNIT_FAIL( std::string( "failed to unlink socket: " ).append( path ) );
	}
}
void sockstreambuf_test::test_local_listen() {
	// socket stream buffer
	sockstreambuf ssb;
	// local (unix) socket address
	const char * path = LOCAL_SOCK_PATH;
	lsockaddr saddr( path );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// bind to address
	try {
		ssb.bind( &saddr );
	} catch ( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
	}
	// listen
	CPPUNIT_ASSERT_NO_THROW( ssb.listen() );
	// close socket
	ssb.close();
	// unlink
	if ( unlink( path ) !=0 ) {
		CPPUNIT_FAIL( std::string( "failed to unlink socket: " ).append( path ) );
	}
}
void sockstreambuf_test::test_local_connect() {
	// socket steam buffer
	sockstreambuf ssb;
	// local (unix) socket address
	const char * path = LOCAL_LISTENER_SOCK_PATH;
	lsockaddr saddr( path );
	// local echo server
	lecho echo( LOCAL_LISTENER_SOCK_PATH );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// connect
	CPPUNIT_ASSERT_NO_THROW( ssb.connect( &saddr ) );
	// close socket
	ssb.close();
}
void sockstreambuf_test::test_local_connect_timeout() {
	// socket steam buffer
	sockstreambuf ssb;
	// local (unix) socket address
	const char * path = LOCAL_LISTENER_SOCK_PATH;
	lsockaddr saddr( path );
	// local echo server
	lecho echo( LOCAL_LISTENER_SOCK_PATH );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// connect
	CPPUNIT_ASSERT_NO_THROW( ssb.connect( &saddr, 1 ) );
	// close socket
	ssb.close();
}
void sockstreambuf_test::test_local_accept() {
	// fork variables
	pid_t  cpid, wpid;
	int    wpid_status;
	// socket stream buffer
	sockstreambuf ssb;
	// local (unix) socket address
	const char * path = LOCAL_SOCK_PATH;
	lsockaddr saddr( path );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// bind to address
	try {
		ssb.bind( &saddr );
	} catch ( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
	}
	// listen
	try {
		ssb.listen();
	} catch ( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
	}
	// fork
	cpid = fork();
	if ( cpid == -1 ) {
		CPPUNIT_FAIL( "failed to fork" );
	} else if ( cpid == 0 ) {
		// child - connect to the local socket created by the parent
		connect_local();
		// exit
		exit( 0 );
	} else {
		// parent - accept a connection from the child
		CPPUNIT_ASSERT( ssb.accept() != -1 );
		// wait for child to exit
		if ( ( wpid = waitpid( cpid, &wpid_status, 0 ) ) == -1 ) {
			CPPUNIT_FAIL( "failed waiting for the child process to terminate" );
		}
	}
	// close socket
	ssb.close();
	// unlink
	if ( unlink( path ) !=0 ) {
		CPPUNIT_FAIL( std::string( "failed to unlink socket: " ).append( path ) );
	}
}
void sockstreambuf_test::test_local_flush() {
	// socket stream buffer
	sockstreambuf ssb;
	// local (unix) socket address
	const char * path = LOCAL_LISTENER_SOCK_PATH;
	lsockaddr saddr( path );
	// local echo server
	lecho echo( LOCAL_LISTENER_SOCK_PATH );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// connect
	try {
		ssb.connect( &saddr );
	} catch ( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// put a char into the buffer and flush
	ssb.sputc( 'c' );
	CPPUNIT_ASSERT( 1 == ssb.flush() );
	// close socket
	ssb.close();
}
void sockstreambuf_test::test_local_flush_read() {
	// socket stream buffer
	sockstreambuf ssb;
	// local (unix) socket address
	const char * path = LOCAL_LISTENER_SOCK_PATH;
	lsockaddr saddr( path );
	// local echo server
	lecho echo( LOCAL_LISTENER_SOCK_PATH );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// connect
	try {
		ssb.connect( &saddr );
	} catch ( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// put a char into the buffer
	ssb.sputc( 'c' );
	// flush
	if ( ssb.flush() == 1 ) {
		// read back
		CPPUNIT_ASSERT( 'c' == ssb.sgetc() );
	} else {
		CPPUNIT_FAIL( "failed to flush buffer" );
	}
	// close socket
	ssb.close();
}
void sockstreambuf_test::test_local_ostream() {
	// socket stream buffer
	sockstreambuf ssb;
	// ostream
	std::ostream output( &ssb );
	// local (unix) socket address
	const char * path = LOCAL_LISTENER_SOCK_PATH;
	lsockaddr saddr( path );
	// local echo server
	lecho echo( LOCAL_LISTENER_SOCK_PATH );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// connect
	try {
		ssb.connect( &saddr );
	} catch ( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// use the output stream to send a char
	output << 'c' << std::endl;
	// read back using the socket stream buffer
	CPPUNIT_ASSERT( 'c' ==  ssb.sgetc() );
	// close socket
	ssb.close();
}
void sockstreambuf_test::test_local_istream() {
	// socket stream buffer
	sockstreambuf ssb;
	// istream
	std::istream input( &ssb );
	// local (unix) socket address
	const char * path = LOCAL_LISTENER_SOCK_PATH;
	lsockaddr saddr( path );
	// local echo server
	lecho echo( LOCAL_LISTENER_SOCK_PATH );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// connect
	try {
		ssb.connect( &saddr );
	} catch ( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// put a char into the buffer
	ssb.sputc( 'c' );
	// flush
	if ( ssb.flush() == 1 ) {
		// read back using istream
		char c;
		input >> c;
		// assert
		CPPUNIT_ASSERT( 'c' == c );
	} else {
		CPPUNIT_FAIL( "failed to flush buffer" );
	}
	// close socket
	ssb.close();
}
void sockstreambuf_test::test_set_timeout() {
	// socket stream buffer
	sockstreambuf ssb;
	time_t sec = 1;
	suseconds_t usec = 500;
	// set timeout
	const timeval * t = ssb.timeout( sec, usec );
	// validate
	CPPUNIT_ASSERT( sec == t->tv_sec );
	CPPUNIT_ASSERT( usec == t->tv_usec );
}
void sockstreambuf_test::test_clear_timeout() {
	// socket stream buffer
	sockstreambuf ssb;
	// clear the timeout before a timeout is set
	CPPUNIT_ASSERT( 0 == ssb.clear_timeout() );
	time_t sec = 1;
	suseconds_t usec = 500;
	// set timeout
	const timeval * t = ssb.timeout( sec, usec );
	// clear the timeout after a timeout is set
	CPPUNIT_ASSERT( 0 == ssb.clear_timeout() );
}
void sockstreambuf_test::test_local_read_timeout() {
	// socket stream buffer
	sockstreambuf ssb;
	// local (unix) socket address
	const char * path = LOCAL_LISTENER_SOCK_PATH;
	lsockaddr saddr( path );
	// local echo server
	lecho echo( LOCAL_LISTENER_SOCK_PATH );
	// prepare the socket
	try {
		ssb.open( sockstreambuf::pf_local, sockstreambuf::sock_stream, sockstreambuf::proto_unspec );
	} catch( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// connect
	try {
		ssb.connect( &saddr );
	} catch ( sockexception &e ) {
		CPPUNIT_FAIL( e.what() );
		return;
	}
	// set timeout
	ssb.timeout( 0, 200 );
	// read - this should timeout and throw a timeout exception
	CPPUNIT_ASSERT_THROW( ssb.sgetc(), socktimeoutexception );
	// should've set the timed-out flag as well
	CPPUNIT_ASSERT( true == ssb.timedout() );
	// close socket
	ssb.close();
}