Michael Granger avatar Michael Granger committed 638309f

Checkpoint commit for work done this morning.

Comments (0)

Files changed (13)

 --- 
-exclude: !ruby/regexp /tmp$|\.(hg|hoerc|env|DS_Store|rvm(rc|\.gems)|pryrc|tm_.*|keep)|(logs|coverage|benchmarks|experiments|\w+-\d+\.\d+\.\d+)\/|config\.yml$|\.sqlite$|newznab_api_specification\.txt|requests\.json/
+exclude: !ruby/regexp /tmp$|run/|\.(hg|hoerc|env|DS_Store|rvm(rc|\.gems)|pryrc|tm_.*|keep)|(logs|coverage|benchmarks|experiments|\w+-\d+\.\d+\.\d+)\/|config\.yml$|\.sqlite$|newznab_api_specification\.txt|requests\.json/
 gli -v2.5.6
 
 builder -v3.2.0
+safe_yaml -v0.9.2
+
 data/newznabr/static/css/app.css
 data/newznabr/static/css/foundation.css
 data/newznabr/static/css/foundation.min.css
+data/newznabr/static/css/general_foundicons.css
 data/newznabr/static/css/loading.gif
 data/newznabr/static/css/normalize.css
+data/newznabr/static/fonts/general_foundicons.eot
+data/newznabr/static/fonts/general_foundicons.svg
+data/newznabr/static/fonts/general_foundicons.ttf
+data/newznabr/static/fonts/general_foundicons.woff
 data/newznabr/static/humans.txt
 data/newznabr/static/index.html
 data/newznabr/static/js/newznabr.js

data/newznabr/static/js/newznabr.js

 		function dashboardController($scope, Release, Binary) {
 			console.log( "Controller for Dashboard running." );
 			$scope.viewname = 'Dashboard';
-			// $scope.releases = Release.query({ limit: 15 });
-			$scope.binaries = Binary.query({ limit: 15, order: 'date' });
+			$scope.releases = Release.query({ limit: 15 });
 		}
 	]).
 	controller('SettingsController', ['$scope', '$log', 'Group', 'ReleaseRegexes',
 	# Version-control revision constant
 	REVISION = %q$Revision$
 
-	# The path to the data directory for the inventory library.
+	# The path to the data directory
 	DATA_DIR = if ENV['NEWZNABR_DATADIR']
 			Pathname( ENV['NEWZNABR_DATADIR'] )
 		elsif Gem.datadir( 'newznabr' )
 	# there are a few specialized methods.
 	#
 
-	### Yield to a block with the Curator logger set to +level+, restoring its original level
+	### Yield to a block with the logger set to +level+, restoring its original level
 	### when the block exits.
 	def self::with_squelched_logger( level=:fatal )
 		orig_level = Loggability[ self ].level

lib/newznabr/binary.rb

 	### +overview+, which should match be of the same form as the Hash returned from
 	### NewzNabr::NNTPClient#overview. The message the +overview+ is from will be
 	### aded as a Part if it isn't already.
+	### :TODO: Refactor
 	def self::from_overview( overview, group )
 		# self.log.debug "Creating a binary and part from an article overview: %p" % [ overview ]
 		return nil unless overview[:article_number] && overview[:subject]

lib/newznabr/db.rb

 		self.with_admin_role do
 			self.connection.create_schema( :newznabr )
 			self.connection << "GRANT ALL PRIVILEGES ON SCHEMA newznabr TO migrator"
+			self.connection << "CREATE EXTENSION IF NOT EXISTS pg_stat_statements"
 		end
 	end
 

lib/newznabr/nntpclient.rb

 	    self.requires_capability( :reader )
 
 		res = self.send_command( :listgroup, group, rngstring )
-		res.expect_status( 211 )
-
-		@current_group = Group.from_response( res )
+		@current_group = Group.from_group_response( res )
 
 		return res.collect {|line| Integer(line.chomp) }
 	end
 	###   }
 	###
 	def list_active( wildmat=nil )
-		self.requires_capability( :reader )
+		self.requires_capability( :list )
 		self.log.debug "Listing active groups, pattern is %p" % [ wildmat ]
 
 		res = self.send_command( :list, :active, wildmat )
 		res.expect_status( 215 )
 
 		return res.each_with_object({}) do |line, hash|
-			# self.log.debug "  got group line: %p" % [ line ]
+			self.log.debug "  got group line: %p" % [ line ]
 			name, hwm, lwm, status = line.chomp.split( /\s+/, 4 )
-			# self.log.debug "  split into: name=%p, hwm=%p, lwm=%p, status=%p" %
-			#    [ name, hwm, lwm, status ]
+			self.log.debug "  split into: name=%p, hwm=%p, lwm=%p, status=%p" %
+			   [ name, hwm, lwm, status ]
 			hash[ name ] = {
 				lwm:    Integer( lwm ),
 				hwm:    Integer( hwm ),

lib/newznabr/nntpclient/group.rb

 	######
 
 	# The group's name
-	attr_reader :name
+	attr_accessor :name
 
 	# The estimated number of articles in the group
-	attr_reader :article_count
+	attr_accessor :article_count
 
 	# The 'high water mark' -- the article number of the newest article.
-	attr_reader :hwm
+	attr_accessor :hwm
+	alias_method :high_water_mark, :hwm
 
 	# THe 'low water mark' -- the article number of the oldest article.
-	attr_reader :lwm
+	attr_accessor :lwm
+	alias_method :low_water_mark, :lwm
 
 
 	### Return +true+ if the group is indicated as empty by the server.
 	def empty?
-		return self.article_count.zero? &&
-			( self.hwm - self.lwm < 1 )
+		# If the group is empty, one of the following three situations will
+		# occur.  Clients MUST accept all three cases; servers MUST NOT
+		# represent an empty group in any other way.
+		#
+		# o  The high water mark will be one less than the low water mark, and
+		#    the estimated article count will be zero.  Servers SHOULD use this
+		#    method to show an empty group.  This is the only time that the
+		#    high water mark can be less than the low water mark.
+		#
+		# o  All three numbers will be zero.
+		#
+		# o  The high water mark is greater than or equal to the low water
+		#    mark.  The estimated article count might be zero or non-zero; if
+		#    it is non-zero, the same requirements apply as for a non-empty
+		#    group.
+		return self.article_count.zero? && (self.hwm - self.lwm < 1)
 	end
 
 

lib/newznabr/releaseregex.rb

 	end
 
 
-	### Return a dataset for NewzNabr::Groups which are matched by the receiver's +groupname+ 
+	### Return a dataset for NewzNabr::Groups which are matched by the receiver's +groupname+
 	### pattern.
 	def groups
 		if self.groupname.include?( '*' )
 	end
 
 
-	### Return an Enumerator for Binaries from the groups which match the receiver's +groupname+ 
+	### Return an Enumerator for Binaries from the groups which match the receiver's +groupname+
 	### and whose subjects match its +regex+. The Enumerator will yield a Binary and the MatchData
 	### from matching the subject.
 	def matching_binaries
 # encoding: utf-8
 # vim: set nosta noet ts=4 sw=4:
 
+# SimpleCov test coverage reporting; enable this using the :coverage rake task
+if ENV['COVERAGE']
+	$stderr.puts "\n\n>>> Enabling coverage report.\n\n"
+	require 'simplecov'
+	SimpleCov.start do
+		add_filter 'spec'
+		add_group "Needing tests" do |file|
+			file.covered_percent < 90
+		end
+	end
+end
+
 require 'pathname'
 require 'time'
 require 'strelka'

spec/newznabr/nntpclient/group_spec.rb

 
 describe NewzNabr::NNTPClient::Group do
 
+	ESTIMATED_ARTICLE_COUNT = 1000
+	LOW_WATER_MARK          = 611
+	HIGH_WATER_MARK         = 1704
+	GROUP_NAME              = 'news.example'
+	RESPONSE_LINE           = [
+		ESTIMATED_ARTICLE_COUNT, LOW_WATER_MARK, HIGH_WATER_MARK, GROUP_NAME
+	].join(' ')
+
+	VALID_GROUP_RESPONSE_CODE = 211
+
+
 	it "can create an instance from a response to an NNTPClient::Response to a GROUP command" do
-		response = NewzNabr::NNTPClient::Response.new( 211, '1000 611 1704 news.example' )
+		response = NewzNabr::NNTPClient::Response.new( VALID_GROUP_RESPONSE_CODE, RESPONSE_LINE )
 		group = described_class.from_group_response( response )
 
-		group.should be_a( described_class )
-		group.name.should == 'news.example'
-		group.article_count.should == 1000
-		group.lwm.should == 611
-		group.hwm.should == 1704
+		expect( group ).to be_a( described_class )
+		expect( group.name ).to eq( GROUP_NAME )
+		expect( group.article_count ).to eq( ESTIMATED_ARTICLE_COUNT )
+		expect( group.low_water_mark ).to eq( LOW_WATER_MARK )
+		expect( group.high_water_mark ).to eq( HIGH_WATER_MARK )
+	end
+
+	it "raises an error if the response to a GROUP command doesn't have a 211 status" do
+		response = NewzNabr::NNTPClient::Response.new( 202, RESPONSE_LINE )
+		expect {
+			described_class.from_group_response(response)
+		}.to raise_error( NewzNabr::NNTP::ProtocolError, /expected 211/i )
 	end
 
 
 
 	describe "instance" do
 
-		let( :group ) { NewzNabr::NNTPClient::Response.new(211, '1000 611 1704 news.example') }
+		let( :group ) { described_class.new('news.example', 1000, 611, 1704) }
+
+
+		it "knows it's empty if its article count, high water mark, and low water mark are all zero" do
+			group.article_count = 0
+			group.hwm = 0
+			group.lwm = 0
+
+			expect( group ).to be_empty
+		end
+
+		it "knows it's empty if the high water mark is one less than the low water mark, and the " +
+		   "estimated article count is zero" do
+			group.article_count = 0
+			group.hwm = 1102
+			group.lwm = 1103
+
+			expect( group ).to be_empty
+		end
+
+		it "can return its high- and low-water marks as a Range" do
+			expect( group.article_range ).to be_a( Range )
+			expect( group.article_range.min ).to eq( LOW_WATER_MARK )
+			expect( group.article_range.max ).to eq( HIGH_WATER_MARK )
+		end
 
 	end
 

spec/newznabr/nntpclient_spec.rb

 	TEST_PORT = Socket.getservbyname( 'nntp' )
 
 	before( :all ) do
-		setup_logging()
+		setup_logging
 	end
 
 
 
 
 	#
-	# Custom matchers
-	#
-
-	RSpec::Matchers.define( :send_command ) do |command|
-		chain :with_message do |message|
-			@message = message
-		end
-
-		match do |client|
-			expect( client.socket ).to receive( :writeline ).with( command )
-		end
-	end
-
-
-	#
 	# Shared Examples
 	#
 
 		described_class.configure( primary: config )
 
 		cl = described_class.for_server( :primary )
-		cl.host.should == config[:host]
-		cl.port.should == config[:port]
+		expect( cl.host ).to eq( config[:host] )
+		expect( cl.port ).to eq( config[:port] )
 	end
 
 	it "can create an SSL-enabled client if the config has the use_ssl option" do
 		described_class.configure( primary: config )
 
 		cl = described_class.for_server( :primary )
-		cl.host.should == config[:host]
-		cl.port.should == config[:port]
-		cl.ssl_enabled?.should be_true()
+
+		expect( cl.host ).to eq( config[:host] )
+		expect( cl.port ).to eq( config[:port] )
+		expect( cl.ssl_enabled? ).to be_true
 	end
 
 	it "can create an authenticated client if the config includes auth info" do
 	end
 
 	it "returns nil when asked to create a non-existant server" do
-		described_class.for_server( :nonexistant ).should be_nil()
+		described_class.for_server( :nonexistant ).should be_nil
 	end
 
 
 				expect( msgio ).to receive( :writeline ).with( 'AUTHINFO pass haqquer' )
 				expect( msgio ).to receive( :readline ).and_return( '281 floppynews server ready' )
 
-				@client.authenticate( 'jrandom', 'haqquer' ).should be_true()
+				@client.authenticate( 'jrandom', 'haqquer' ).should be_true
 			end
 		end
 
 				expect( msgio ).to receive( :writeline ).with( 'AUTHINFO user jrandom' )
 				expect( msgio ).to receive( :readline ).and_return( '281 floppynews server ready' )
 
-				@client.authenticate( 'jrandom' ).should be_true()
+				@client.authenticate( 'jrandom' ).should be_true
 			end
 		end
 
 			end
 		end
 
-		it "can fetch the server's HELP text" do
-			with_connected_client do |msgio|
-				@client.instance_variable_set( :@capabilities, reader: [], version: ['2'] )
-
-				expect( msgio ).to receive( :writeline ).with( 'HELP' )
-				expect( msgio ).to receive( :readline ).and_return( '100 help (GC 3.4.22)' )
-				expect( msgio ).to receive( :each_message_chunk ).
-					and_yield( '  ARTICLE [<msgid>|number]' ).
-					and_yield('  AUTHINFO type value' )
-
-				@client.help.should =~ /AUTHINFO type value/
-			end
-		end
-
-
-		it "can set the current group" do
-			with_connected_client do |msgio|
-				@client.instance_variable_set( :@capabilities, reader: [], version: ['2'] )
-
-				expect( msgio ).to receive( :writeline ).with( 'GROUP misc.test' )
-				expect( msgio ).to receive( :readline ).and_return( '211 1234 3000234 3002322 misc.test' )
-
-				@client.group( 'misc.test' )
-				@client.current_group.name.should == 'misc.test'
-			end
-		end
-
-
-		it 'can fetch the list of groups'
-
 		it "automatically sets the SSL version to :TLSv1.1 if connecting on the default port" do
 			ctx = double( "SSL context" )
 			expect( OpenSSL::SSL::SSLContext ).to receive( :new ).and_return( ctx )
 			@client.ssl_enabled = true
 		end
 
+		it "resets the SSL context when SSL is disabled" do
+			ctx = double( "SSL context" )
+			allow( OpenSSL::SSL::SSLContext ).to receive( :new ).and_return( ctx )
+			allow( ctx ).to receive( :ssl_version= ).with( :SSLv23_client )
+
+			@client.port = 563
+			@client.ssl_enabled = false
+
+			expect( @client.ssl_context ).to be_nil
+		end
+
 
 		context "with an SSL context set" do
 
 				@client.port = 563
 
 				with_fixtured_socket do |socket|
-					wrapped_socket = mock( "SSL socket wrapper")
+					wrapped_socket = double( "SSL socket wrapper")
 					allow( wrapped_socket ).to receive( :sync_close= )
 					allow( wrapped_socket ).to receive( :connect ).and_return( true )
 
 				end
 			end
 
-
 			it "attempts to start TLS if connecting to the non-SSL port" do
 				with_fixtured_socket do |socket|
 					# Use a plain message IO first
 					# Now the upgrade happens
 					expect( msgio ).to receive( :writeline ).with( "STARTTLS" )
 
-					wrapped_socket = mock( "SSL socket wrapper" )
+					wrapped_socket = double( "SSL socket wrapper" )
 					allow( wrapped_socket ).to receive( :sync_close= )
 					allow( wrapped_socket ).to receive( :connect ).and_return( true )
 
 
 		end
 
+
+		it "can fetch the server's HELP text" do
+			with_connected_client do |msgio|
+				@client.instance_variable_set( :@capabilities, reader: [], version: ['2'] )
+
+				expect( msgio ).to receive( :writeline ).with( 'HELP' )
+				expect( msgio ).to receive( :readline ).and_return( '100 help (GC 3.4.22)' )
+				expect( msgio ).to receive( :each_message_chunk ).
+					and_yield( '  ARTICLE [<msgid>|number]' ).
+					and_yield('  AUTHINFO type value' )
+
+				@client.help.should =~ /AUTHINFO type value/
+			end
+		end
+
+
+		it "can set the current group" do
+			with_connected_client do |msgio|
+				@client.instance_variable_set( :@capabilities, reader: [], version: ['2'] )
+
+				expect( msgio ).to receive( :writeline ).with( 'GROUP misc.test' )
+				expect( msgio ).to receive( :readline ).and_return( '211 1234 3000234 3002322 misc.test' )
+
+				@client.group( 'misc.test' )
+				@client.current_group.name.should == 'misc.test'
+			end
+		end
+
+
+		it 'can fetch the list of all active groups from the server' do
+			with_connected_client do |msgio|
+				@client.instance_variable_set( :@capabilities, list: [] )
+
+				expect( msgio ).to receive( :writeline ).with( 'LIST active' )
+				expect( msgio ).to receive( :readline ).and_return( '215 list of newsgroups follows' )
+				expect( msgio ).to receive( :each_message_chunk ).
+					and_yield( 'misc.test 3002322 3000234 y' ).
+					and_yield( 'comp.risks 442001 441099 m' ).
+					and_yield( 'alt.rfc-writers.recovery 4 1 y' ).
+					and_yield( 'tx.natives.recovery 89 56 y' ).
+					and_yield( 'tx.natives.recovery.d 11 9 n' )
+
+				result = @client.list_active
+				expect( result ).to eq({
+					"misc.test"                => { lwm: 3000234, hwm: 3002322, status: "y" },
+					"comp.risks"               => { lwm: 441099,  hwm: 442001,  status: "m" },
+					"alt.rfc-writers.recovery" => { lwm: 1,       hwm: 4,       status: "y" },
+					"tx.natives.recovery"      => { lwm: 56,      hwm: 89,      status: "y" },
+					"tx.natives.recovery.d"    => { lwm: 9,       hwm: 11,      status: "n" }
+				})
+			end
+		end
+
+
+		it 'can fetch the list of active groups which match a pattern from the server' do
+			with_connected_client do |msgio|
+				@client.instance_variable_set( :@capabilities, list: [] )
+
+				expect( msgio ).to receive( :writeline ).with( 'LIST active tx.*' )
+				expect( msgio ).to receive( :readline ).and_return( '215 list of newsgroups follows' )
+				expect( msgio ).to receive( :each_message_chunk ).
+					and_yield( 'tx.natives.recovery 89 56 y' ).
+					and_yield( 'tx.natives.recovery.d 11 9 n' )
+
+				result = @client.list_active( 'tx.*' )
+				expect( result ).to eq({
+					"tx.natives.recovery"      => { lwm: 56, hwm: 89, status: "y" },
+					"tx.natives.recovery.d"    => { lwm: 9,  hwm: 11, status: "n" }
+				})
+			end
+		end
+
+
+		it "raises an exception when listing active groups if the server doesn't have LIST capability" do
+			with_connected_client do |msgio|
+				@client.instance_variable_set( :@capabilities, reader: [] ) # (no LIST)
+				expect { @client.list_active }.
+					to raise_error( NewzNabr::NNTP::CapabilityMissing, /doesn't support the list/i )
+			end
+		end
+
+
+		it "can fetch a list of current article numbers from the current group" do
+			with_connected_client do |msgio|
+				@client.instance_variable_set( :@capabilities, reader: [] )
+
+				expect( msgio ).to receive( :writeline ).with( 'LISTGROUP' )
+				expect( msgio ).to receive( :readline ).
+					and_return( '211 2000 3000234 3002322 misc.test list follows' )
+				expect( msgio ).to receive( :each_message_chunk ).
+					and_yield( '3000234' ).
+					and_yield( '3000237' ).
+					and_yield( '3000238' ).
+					and_yield( '3000239' ).
+					and_yield( '3002322' )
+
+				result = @client.listgroup
+				expect( result ).to eq([3000234, 3000237, 3000238, 3000239, 3002322])
+			end
+		end
+
+
+		it "can fetch a list of current article numbers from a group other than the current one"
+		it "can fetch a range of current article numbers from a group"
+
+		it "raises an exception when listing article numbers if the server doesn't have READER capability"
+
 	end
 
 end
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.