pwl 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ require 'helper'
2
+ require 'nokogiri/diff'
3
+
4
+ # Tests `pwl export`
5
+ class TestExport < Test::Pwl::AppTestCase
6
+ def test_empty
7
+ fixture = fixture("test_empty.html").gsub('CREATED_STAMP', DateTime.now.strftime('%F %R')).gsub('MODIFIED_STAMP', 'never').gsub('DATABASE_FILE', store_file)
8
+ assert_successful_html(fixture, 'export')
9
+ end
10
+
11
+ def test_all
12
+ test_vector = Hash['foo', 'one', 'bar', 'two', 'Chuck Norris', 'Roundhouse Kick']
13
+ test_vector.each{|k,v|
14
+ assert_successful('', "put '#{k}' '#{v}'")
15
+ }
16
+
17
+ now = DateTime.now.strftime('%F %R')
18
+ fixture = fixture("test_all.html").gsub('CREATED_STAMP', now).gsub('MODIFIED_STAMP', now).gsub('DATABASE_FILE', store_file)
19
+
20
+ assert_successful_html(fixture, 'export')
21
+ end
22
+
23
+ def assert_successful_html(expected_out, cmd, password = store_password)
24
+ out, err, rc = execute(cmd, password)
25
+ assert_equal(0, rc.exitstatus, "Expected exit status 0, but it was #{rc.exitstatus}. STDERR was: #{err}")
26
+ assert(err.empty?, "Expected empty STDERR, but it yielded #{err}")
27
+
28
+ actual = Nokogiri::HTML(out)
29
+ expected = Nokogiri::HTML(expected_out)
30
+
31
+ differences = actual.diff(expected, :added => true, :removed => true)
32
+
33
+ assert_equal(0, differences.count, "Unexpected differences in output. Diff:\n" << differences.collect{|change, node|
34
+ case change
35
+ when '+'
36
+ "Actual: #{node.to_html}"
37
+ when '-'
38
+ "Expected: #{node.to_html}"
39
+ end
40
+ }.join("\n"))
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ require 'helper'
2
+
3
+ # Tests `pwl get`
4
+ class TestGet < Test::Pwl::AppTestCase
5
+ def test_get_unknown_key
6
+ assert_error('No entry was found for foo', 'get foo')
7
+ end
8
+
9
+ def test_get_blank_key
10
+ assert_error('may not be blank', 'get')
11
+ end
12
+
13
+ def test_get_known_key
14
+ assert_successful('', 'put foo bar') # TODO Use a fixture instead of put
15
+ assert_successful('^bar$', 'get foo')
16
+ end
17
+ end
@@ -0,0 +1,81 @@
1
+ require 'helper'
2
+ require 'pty'
3
+ require 'expect'
4
+
5
+ class TestInit < Test::Pwl::AppTestCase
6
+ #
7
+ # Tests that initing with two matching passwords succeeds
8
+ #
9
+ # A session is expected to look like this:
10
+ #
11
+ # $ bin/pwl init --force --verbose
12
+ # Enter new master password:
13
+ # **************
14
+ # Enter master password again:
15
+ # **************
16
+ # Successfully initialized new store at ...
17
+ # $
18
+ #
19
+ def test_matching_passwords
20
+ cmd = "bin/pwl init --force --verbose --file \"#{@store_file}\""
21
+
22
+ PTY.spawn(cmd){|pwl_out, pwl_in, pid|
23
+ assert_response('Enter new master password:', pwl_out)
24
+ pwl_in.puts("secr3tPassw0rd")
25
+
26
+ assert_response('Enter master password again:', pwl_out)
27
+ pwl_in.puts("secr3tPassw0rd")
28
+
29
+ assert_response('Successfully initialized new store', pwl_out)
30
+ }
31
+ end
32
+
33
+ #
34
+ # Tests that initing with two passwords that don't match fails
35
+ #
36
+ # A session is expected to look like this (using s3cretPassw0rd at the first and secr3tPassw0rd at the second password prompt):
37
+ #
38
+ # $ bin/pwl init --force --verbose
39
+ # Enter new master password:
40
+ # **************
41
+ # Enter master password again:
42
+ # **************
43
+ # Passwords do not match.
44
+ # $
45
+ #
46
+ def test_unmatching_passwords
47
+ cmd = "bin/pwl init --force --verbose --file \"#{store_file}\""
48
+
49
+ PTY.spawn(cmd){|pwl_out, pwl_in, pid|
50
+ assert_response('Enter new master password:', pwl_out)
51
+ pwl_in.puts("s3cretPassw0rd")
52
+
53
+ assert_response('Enter master password again:', pwl_out)
54
+ pwl_in.puts("secr3tPassw0rd")
55
+
56
+ assert_response('Passwords do not match\.', pwl_out)
57
+ }
58
+ end
59
+
60
+ #
61
+ # Tests that initing an existing store without --force does not touch the existing store
62
+ #
63
+ def test_exists
64
+ assert_error('already exists', "init")
65
+ end
66
+
67
+ #
68
+ # Tests that cancelling a forced re-init does not change the store file
69
+ #
70
+ def test_cancel
71
+ # TODO
72
+ end
73
+
74
+ private
75
+
76
+ def assert_response(expected, actual_io)
77
+ if !actual_io.expect(/#{expected}/, 1)
78
+ raise StandardError.new("Expected response '#{expected}' was not matched")
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,30 @@
1
+ require 'helper'
2
+
3
+ # Tests `pwl list`
4
+ class TestList < Test::Pwl::AppTestCase
5
+ def test_list_empty
6
+ assert_empty(store.list)
7
+ assert_error('^$', 'list')
8
+ assert_error('^List is empty\.$', 'list --verbose')
9
+ end
10
+
11
+ def test_list_all
12
+ test_vector = Hash['foo', 'one', 'bar', 'two', 'Chuck Norris', 'Roundhouse Kick']
13
+ test_vector.each{|k,v|
14
+ assert_successful('', "put '#{k}' '#{v}'")
15
+ }
16
+
17
+ assert_successful(test_vector.keys.join('-'), 'list -s "-"')
18
+ end
19
+
20
+ def test_list_filter
21
+ test_vector = Hash['foo', 'one', 'foot', 'two', 'Homer Simpson', 'Apu Nahasapeemapetilon']
22
+ test_vector.each{|k,v|
23
+ assert_successful('', "put '#{k}' '#{v}'")
24
+ }
25
+
26
+ filter = 'foo'
27
+ expected = test_vector.keys.select{|k,v| k =~ /#{filter}/}.join(',')
28
+ assert_successful("^#{expected}$", "list #{filter} -s ,")
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ require 'helper'
2
+
3
+ # Tests `pwl passwd`
4
+ class TestPasswd < Test::Pwl::AppTestCase
5
+ def test_blank_password
6
+ assert_error('May not be blank', 'passwd')
7
+ end
8
+
9
+ def test_standard
10
+ assert_successful('', 'put foo bar')
11
+
12
+ new_pwd = store_password.reverse
13
+
14
+ # If we are in a pipe (and we are in these tests), the new password is expected as first arg
15
+ assert_successful('^$', "passwd \"#{new_pwd}\"")
16
+
17
+ # the old password must not work anymore
18
+ assert_error('The master password is wrong', 'list')
19
+
20
+ # re-open with the changed password
21
+ assert_successful('^bar$', 'get foo', new_pwd)
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ require 'helper'
2
+
3
+ # Tests `pwl put`
4
+ class TestPut < Test::Pwl::AppTestCase
5
+ def test_put_blank_key
6
+ assert_error('may not be blank', 'put')
7
+ end
8
+
9
+ def test_put_simple
10
+ assert_successful('', 'put foo bar')
11
+ assert_successful('^bar$', 'get foo')
12
+ end
13
+
14
+ def test_put_update
15
+ assert_successful('', 'put foo bar')
16
+ assert_successful('', 'put foo baz') # just do it twice
17
+ assert_successful('^baz$', 'get foo')
18
+ end
19
+ end
@@ -0,0 +1,71 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5
+ <title>Password Manager Export</title>
6
+ <style type="text/css">
7
+ table { page-break-inside:auto}
8
+ tr { page-break-inside:avoid; page-break-after:auto}
9
+ thead { display:table-header-group}
10
+ tfoot { display:table-footer-group; font-size: 0.85em;}
11
+
12
+ /* adapted from http://coding.smashingmagazine.com/2008/08/13/top-10-css-table-designs/ */
13
+ body{
14
+ font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
15
+ font-size: 12px;
16
+ margin: 45px;
17
+ width: 480px;
18
+ border-collapse: collapse;
19
+ text-align: left;
20
+ }
21
+ th{
22
+ font-size: 14px;
23
+ font-weight: bold;
24
+ padding: 10px 8px;
25
+ border-bottom: 2px solid;
26
+ }
27
+ td{
28
+ border-bottom: 1px solid;
29
+ padding: 6px 8px;
30
+ }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <h1>Password Manager Export</h1>
35
+ <table>
36
+ <thead>
37
+ <tr>
38
+ <th>Name</th>
39
+ <th>Value</th>
40
+ </tr>
41
+ </thead>
42
+ <tfoot>
43
+ <tr>
44
+ <td colspan="2">
45
+ This <a href="http://rdoc.info/github/nerab/pwl/master/frames">pwl</a> export was created on CREATED_STAMP.
46
+ <br/>
47
+ The database at DATABASE_FILE was last modified MODIFIED_STAMP.
48
+ </td>
49
+ </tr>
50
+ </tfoot>
51
+ <tbody>
52
+
53
+ <tr>
54
+ <td>foo</td>
55
+ <td>one</td>
56
+ </tr>
57
+
58
+ <tr>
59
+ <td>bar</td>
60
+ <td>two</td>
61
+ </tr>
62
+
63
+ <tr>
64
+ <td>Chuck Norris</td>
65
+ <td>Roundhouse Kick</td>
66
+ </tr>
67
+
68
+ </tbody>
69
+ </table>
70
+ </body>
71
+ </html>
@@ -0,0 +1,56 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5
+ <title>Password Manager Export</title>
6
+ <style type="text/css">
7
+ table { page-break-inside:auto}
8
+ tr { page-break-inside:avoid; page-break-after:auto}
9
+ thead { display:table-header-group}
10
+ tfoot { display:table-footer-group; font-size: 0.85em;}
11
+
12
+ /* adapted from http://coding.smashingmagazine.com/2008/08/13/top-10-css-table-designs/ */
13
+ body{
14
+ font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
15
+ font-size: 12px;
16
+ margin: 45px;
17
+ width: 480px;
18
+ border-collapse: collapse;
19
+ text-align: left;
20
+ }
21
+ th{
22
+ font-size: 14px;
23
+ font-weight: bold;
24
+ padding: 10px 8px;
25
+ border-bottom: 2px solid;
26
+ }
27
+ td{
28
+ border-bottom: 1px solid;
29
+ padding: 6px 8px;
30
+ }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <h1>Password Manager Export</h1>
35
+ <table>
36
+ <thead>
37
+ <tr>
38
+ <th>Name</th>
39
+ <th>Value</th>
40
+ </tr>
41
+ </thead>
42
+ <tfoot>
43
+ <tr>
44
+ <td colspan="2">
45
+ This <a href="http://rdoc.info/github/nerab/pwl/master/frames">pwl</a> export was created on CREATED_STAMP.
46
+ <br/>
47
+ The database at DATABASE_FILE was never modified.
48
+ </td>
49
+ </tr>
50
+ </tfoot>
51
+ <tbody>
52
+
53
+ </tbody>
54
+ </table>
55
+ </body>
56
+ </html>
data/test/helper.rb ADDED
@@ -0,0 +1,79 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'rubygems'
5
+ require 'bundler'
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+ require 'test/unit'
14
+
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
17
+ require 'pwl'
18
+ require 'tmpdir'
19
+
20
+ class Test::Unit::TestCase
21
+ FIXTURES_DIR = File.join(File.dirname(__FILE__), 'fixtures')
22
+ end
23
+
24
+ module Test
25
+ module Pwl
26
+ class TestCase < Test::Unit::TestCase
27
+ attr_reader :store, :store_file
28
+
29
+ def setup
30
+ @store_file = temp_file_name
31
+ @store = ::Pwl::Store.new(@store_file, store_password)
32
+ end
33
+
34
+ def teardown
35
+ File.unlink(@store_file)
36
+ end
37
+
38
+ def store_password
39
+ 's3cret passw0rd'
40
+ end
41
+
42
+ # Make up a name of a file that does not exist in ENV['TMPDIR'] yet
43
+ def temp_file_name
44
+ begin
45
+ result = File.join(Dir.tmpdir, "#{self.class.name}-#{Random.rand}.pstore")
46
+ end while File.exists?(result)
47
+ result
48
+ end
49
+ end
50
+
51
+ class AppTestCase < TestCase
52
+ APP = 'bin/pwl'
53
+
54
+ protected
55
+
56
+ def assert_successful(expected_out, cmd, password = store_password)
57
+ out, err, rc = execute(cmd, password)
58
+ assert_equal(0, rc.exitstatus, "Expected exit status 0, but it was #{rc.exitstatus}. STDERR was: #{err}")
59
+ assert(err.empty?, "Expected empty STDERR, but it yielded #{err}")
60
+ assert(out =~ /#{expected_out}/, "'#{out}' did not match expected response '#{expected_out}'")
61
+ end
62
+
63
+ def assert_error(expected_err, cmd, password = store_password)
64
+ out, err, rc = execute(cmd, password)
65
+ assert_not_equal(0, rc.exitstatus, "Expected non-zero exit status, but it was #{rc.exitstatus}. STDOUT was: #{out}")
66
+ assert(out.empty?, "Expected empty STDOUT, but it yielded #{out}")
67
+ assert(err =~ /#{expected_err}/, "'#{err}' did not match expected response '#{expected_err}'")
68
+ end
69
+
70
+ def execute(cmd, password)
71
+ Open3.capture3("echo \"#{password}\" | #{APP} #{cmd} --file \"#{store_file}\"")
72
+ end
73
+
74
+ def fixture(name)
75
+ File.read(File.join(FIXTURES_DIR, name))
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,29 @@
1
+ require 'helper'
2
+
3
+ class TestError < Test::Unit::TestCase
4
+ def setup
5
+ @message = Pwl::ErrorMessage.new("<%= first %> <%= last %>", 1, :first => 'FIRSTNAME', :last => 'LASTNAME')
6
+ end
7
+
8
+ def test_code_0
9
+ assert_raise Pwl::ReservedMessageCodeError do
10
+ Pwl::ErrorMessage.new('', 0)
11
+ end
12
+ end
13
+
14
+ def test_to_s
15
+ assert_equal('Mislav Marohnic', @message.to_s(:first => "Mislav", "last" => "Marohnic"))
16
+ end
17
+
18
+ def test_to_s_empty
19
+ assert_equal('FIRSTNAME LASTNAME', @message.to_s)
20
+ end
21
+
22
+ def test_code
23
+ assert_equal(1, @message.exit_code)
24
+ end
25
+
26
+ def test_to_s_extra
27
+ assert_equal('Apu Nahasapeemapetilon', @message.to_s({:first => 'Apu', 'last' => 'Nahasapeemapetilon', :worklocation => 'Kwik-E-Mart'}))
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ require 'helper'
2
+
3
+ class TestMessageZero < Test::Unit::TestCase
4
+ def setup
5
+ @msg = Pwl::Message.new("Name: <%= first %> <%= last %>", 0, :first => 'FIRSTNAME', :last => 'LASTNAME')
6
+ end
7
+
8
+ def test_to_s
9
+ assert_equal('Name: Mislav Marohnic', @msg.to_s(:first => "Mislav", "last" => "Marohnic"))
10
+ end
11
+
12
+ def test_to_s_default
13
+ assert_equal('Name: FIRSTNAME LASTNAME', @msg.to_s)
14
+ end
15
+
16
+ def test_code
17
+ assert_equal(0, @msg.exit_code)
18
+ end
19
+
20
+ def test_to_s_extra
21
+ assert_equal('Name: Apu Nahasapeemapetilon', @msg.to_s({:first => 'Apu', 'last' => 'Nahasapeemapetilon', :worklocation => 'Kwik-E-Mart'}))
22
+ end
23
+ end
24
+
25
+ class TestMessageNonZero < Test::Unit::TestCase
26
+ def setup
27
+ @code = Random.rand(255)
28
+ @msg = Pwl::Message.new("Name: <%= first %> <%= last %>", @code)
29
+ end
30
+
31
+ def test_code
32
+ assert_equal(@code, @msg.exit_code)
33
+ end
34
+ end
@@ -0,0 +1,62 @@
1
+ require 'helper'
2
+ require 'tempfile'
3
+
4
+ class TestStoreConstruction < Test::Pwl::TestCase
5
+ def test_existing_store
6
+ assert_raise Pwl::Store::FileAlreadyExistsError do
7
+ Pwl::Store.new(store_file, store_password)
8
+ end
9
+ end
10
+
11
+ def test_nonexisting_store
12
+ assert_raise Pwl::Store::FileNotFoundError do
13
+ Pwl::Store.open(temp_file_name, store_password)
14
+ end
15
+ end
16
+
17
+ # fake level (increasingly close to structure)
18
+ USER = 1 # user root exists
19
+ SYSTEM = 2 # user and system root exists
20
+ CREATED = 3 # user and system root exists, system root contains created stamp
21
+ SALT = 4 # like above, plus salt is set to random value
22
+
23
+ def test_existing_uninitialized_store
24
+ {USER => Pwl::Store::NotInitializedError,
25
+ SYSTEM => Pwl::Store::NotInitializedError,
26
+ CREATED => Pwl::Store::NotInitializedError,
27
+ SALT => Pwl::Store::WrongMasterPasswordError,
28
+ }.each{|fake_level, error| assert assert_uninitialized(fake_level, error)}
29
+ end
30
+
31
+ private
32
+
33
+ def assert_uninitialized(fake_level, error)
34
+ existing_file = Tempfile.new(self.class.name)
35
+
36
+ begin
37
+ fake_store(existing_file, fake_level)
38
+
39
+ assert_raise(error) do
40
+ Pwl::Store.open(existing_file, store_password)
41
+ end
42
+ ensure
43
+ existing_file.close
44
+ existing_file.unlink
45
+ end
46
+ end
47
+
48
+ # Pretend that we have a store with correct layout
49
+ def fake_store(existing_file, fake_level)
50
+ store = PStore.new(existing_file)
51
+ store.transaction{
52
+ store.commit if fake_level < USER
53
+ store[:user] = {}
54
+ store.commit if fake_level < SYSTEM
55
+ store[:system] = {}
56
+ store.commit if fake_level < CREATED
57
+ store[:system][:created] = DateTime.now
58
+ store.commit if fake_level < SALT
59
+ store[:system][:salt] = Random.rand.to_s
60
+ }
61
+ end
62
+ end
@@ -0,0 +1,90 @@
1
+ require 'helper'
2
+
3
+ class TestStoreCRUD < Test::Pwl::TestCase
4
+ def test_put_get
5
+ store.put('foo', 'bar')
6
+ assert_equal('bar', store.get('foo'))
7
+ end
8
+
9
+ def test_get_blank
10
+ assert_raise Pwl::Store::BlankKeyError do
11
+ store.get('')
12
+ end
13
+
14
+ assert_raise Pwl::Store::BlankKeyError do
15
+ store.get(nil)
16
+ end
17
+ end
18
+
19
+ def test_put_get_blank
20
+ assert_raise Pwl::Store::BlankValueError do
21
+ store.put('empty', '')
22
+ end
23
+
24
+ assert_raise Pwl::Store::BlankValueError do
25
+ store.put('nil', nil)
26
+ end
27
+ end
28
+
29
+ def test_unknown_key
30
+ assert_raise Pwl::Store::KeyNotFoundError do
31
+ store.get('foo')
32
+ end
33
+ end
34
+
35
+ def test_list_empty
36
+ assert_empty(store.list)
37
+ end
38
+
39
+ def test_list
40
+ test_vector = Hash['foo', 'one', 'bar', 'two', 'Chuck Norris', 'Roundhouse Kick']
41
+ test_vector.each{|k,v| store.put(k, v)}
42
+ assert_equal(test_vector.keys, store.list)
43
+ store.list.each{|key|
44
+ assert_equal(test_vector[key], store.get(key))
45
+ }
46
+ end
47
+
48
+ def test_all
49
+ test_vector = Hash['foo', 'one', 'bar', 'two', 'Chuck Norris', 'Roundhouse Kick']
50
+ test_vector.each{|k,v| store.put(k, v)}
51
+ assert_equal(test_vector, store.all)
52
+ store.all.each{|k,v|
53
+ assert_equal(test_vector[k], v)
54
+ }
55
+ end
56
+
57
+ def test_list_filter
58
+ test_vector = Hash['foo', 'one', 'bar', 'two', 'Chuck Norris', 'Roundhouse Kick']
59
+ test_vector.each{|k,v| store.put(k, v)}
60
+
61
+ filter = 'foo bar'
62
+ expected = test_vector.keys.select{|k,v| k =~ /#{filter}/}
63
+ assert_equal(expected, store.list(filter))
64
+ end
65
+
66
+ def test_delete
67
+ store.put('foo', 'bar')
68
+ assert_equal('bar', store.delete('foo'))
69
+
70
+ assert_raise Pwl::Store::KeyNotFoundError do
71
+ store.get('foo')
72
+ end
73
+ end
74
+
75
+ def test_delete_blank
76
+ assert_raise Pwl::Store::BlankKeyError do
77
+ store.delete('')
78
+ end
79
+
80
+ assert_raise Pwl::Store::BlankKeyError do
81
+ store.delete(nil)
82
+ end
83
+ end
84
+
85
+ def test_delete_unknown_key
86
+ assert_raise Pwl::Store::KeyNotFoundError do
87
+ store.delete('foo')
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,35 @@
1
+ require 'helper'
2
+
3
+ class TestStoreConstruction < Test::Pwl::TestCase
4
+ # when comparing timestamps, allow not more than this difference in seconds
5
+ TIMESTAMP_PRECISION = 1
6
+
7
+ def test_created
8
+ assert_equal(nil, store.last_accessed)
9
+ assert_in_delta(DateTime.now.to_time.to_i, store.created.to_time.to_i, TIMESTAMP_PRECISION)
10
+ end
11
+
12
+ def test_last_accessed
13
+ assert_equal(nil, store.last_accessed)
14
+ store.put('foobar', 'barfoot')
15
+ assert_equal(nil, store.last_accessed)
16
+ store.get('foobar')
17
+ assert_in_delta(DateTime.now.to_time.to_i, store.last_accessed.to_time.to_i, TIMESTAMP_PRECISION)
18
+ end
19
+
20
+ def test_last_accessed_nonexisting
21
+ assert_equal(nil, store.last_accessed)
22
+ assert_raise Pwl::Store::KeyNotFoundError do
23
+ store.get('foobar')
24
+ end
25
+
26
+ # Make sure a failed read is not counted as last_accessed
27
+ assert_equal(nil, store.last_accessed)
28
+ end
29
+
30
+ def test_last_modified
31
+ assert_equal(nil, store.last_modified)
32
+ store.put('foobar', 'barfoot')
33
+ assert_in_delta(DateTime.now.to_time.to_i, store.last_modified.to_time.to_i, TIMESTAMP_PRECISION)
34
+ end
35
+ end