Index: /trunk/cpanfile
===================================================================
--- /trunk/cpanfile	(revision 91)
+++ /trunk/cpanfile	(revision 92)
@@ -7,4 +7,7 @@
 requires 'Template';
 requires 'Encode';
+
+# web application
+requires 'Plack', '1.0036';
 
 # for opening the dbh handle
Index: /trunk/lib/Bookmarks.pm
===================================================================
--- /trunk/lib/Bookmarks.pm	(revision 91)
+++ /trunk/lib/Bookmarks.pm	(revision 92)
@@ -39,6 +39,6 @@
     my $self = shift;
     require File::Slurp;
-    require FindBin;
-    my $table_definitions = File::Slurp::read_file("$FindBin::RealBin/bookmarks.sql");
+    require File::Basename;
+    my $table_definitions = File::Slurp::read_file(File::Basename::dirname($INC{'Bookmarks.pm'}) . "/../bookmarks.sql");
     $self->dbh->{sqlite_allow_multiple_statements} = 1;
     $self->dbh->do($table_definitions);
Index: /trunk/lib/BookmarksApp.pm
===================================================================
--- /trunk/lib/BookmarksApp.pm	(revision 91)
+++ /trunk/lib/BookmarksApp.pm	(revision 92)
@@ -17,8 +17,16 @@
     my $self = shift;
 
-    -e $self->config_file or die "Config file " . $self->config_file . " not found\n";
+    my $config = $self->config;
 
-    my $config = YAML::LoadFile($self->config_file);
-    $self->config($config);
+    # if the config_file is set, try to load the config from there
+    # note that currently, if there is a config_file and a config,
+    # all of the values in config are ignored in favor of those in
+    # the config_file
+    if ($self->config_file) {
+        -e $self->config_file or die "Config file " . $self->config_file . " not found\n";
+        $config = YAML::LoadFile($self->config_file);
+        #TODO: merge the configs instead of overwriting?
+        $self->config($config);
+    }
 
     my $router = router {
@@ -75,5 +83,6 @@
     $self->_app(
         builder {
-            enable_if { $_[0]->{REMOTE_ADDR} eq $config->{proxy_ip} } 'ReverseProxy';
+            enable_if { $_[0]->{REMOTE_ADDR} eq $config->{proxy_ip} } 'ReverseProxy'
+                if $config->{proxy_ip};
             enable_if { $_[0]->{REQUEST_METHOD} ne 'GET' } 'Auth::Digest', (
                 realm           => 'Bookmarks',
@@ -81,5 +90,5 @@
                 password_hashed => 1,
                 authenticator   => sub { $config->{digest_password} }
-            );
+            ) if $config->{auth};
             sub { $router->dispatch(shift); };
         }
Index: /trunk/t/00-cgiapp.t
===================================================================
--- /trunk/t/00-cgiapp.t	(revision 92)
+++ /trunk/t/00-cgiapp.t	(revision 92)
@@ -0,0 +1,120 @@
+#!/usr/bin/perl -w
+use strict;
+
+use Plack::Test;
+use HTTP::Request::Common;
+
+use Test::More;
+#use Test::WWW::Mechanize::CGIApp;
+use Test::JSON;
+use Digest::MD5 qw{md5_hex};
+
+# test fixture setup
+my $dbname = 't/apptest.db';
+unlink $dbname if -e $dbname;
+my $bookmarks = Bookmarks->new({ dbname => $dbname });
+$bookmarks->create_tables;
+my $username = 'apptest';
+my $password = 'apptest';
+
+# app setup
+use BookmarksApp;
+my $app = BookmarksApp->new({
+    config => {
+        dbname          => $dbname,
+        #auth            => 1,
+        digest_key      => 'test_secret',
+        digest_password => md5_hex("$username:Bookmarks:$password"),
+        #proxy_ip        => '',
+    }
+})->to_app;
+
+my $test = Plack::Test->create($app);
+
+my $res = $test->request(GET "/");
+is($res->content_type, 'text/html');
+
+$res = $test->request(GET '/?format=json');
+is($res->content_type, 'application/json', 'JSON content type is application/json');
+is_valid_json $res->content;
+is_json $res->content, '{ "bookmarks": [] }';
+
+$res = $test->request(GET '/?format=xbel');
+is($res->content_type, 'application/xml', 'XBEL content type is application/xml');
+
+$res = $test->request(GET '/?format=text');
+is($res->content_type, 'text/uri-list', 'Text content type is text/uri-list');
+
+
+my $bookmark_uri;
+
+subtest 'Created bookmark' => sub {
+    # create a bookmark
+    my $res = $test->request(POST
+        '/',
+        {
+            uri => 'http://metacpan.org',
+            title => 'My Bookmark',
+            tags => 'test',
+        },
+    );
+
+    $bookmark_uri = $res->header('Location'); #->as_string;
+
+    $res = $test->request(GET "$bookmark_uri/title");
+    is($res->content_type, 'text/plain', 'Title is text/plain');
+    is($res->content, 'My Bookmark', 'Title has correct value');
+    $res = $test->request(GET "$bookmark_uri/uri");
+    is($res->content_type, 'text/plain', 'URI is text/plain');
+    is($res->content, 'http://metacpan.org', 'URI has correct value');
+    $res = $test->request(GET "$bookmark_uri/tags");
+    is($res->content_type, 'text/plain', 'Tags are text/plain');
+    is($res->content, 'test', 'Tags have correct value');
+};
+
+subtest 'Updated bookmark' => sub {
+    my $res = $test->request(
+        POST $bookmark_uri,
+        {
+            uri => 'http://search.cpan.org',
+            title => 'My Other Bookmark',
+            tags => 'test one two',
+        },
+    );
+    $res = $test->request(GET "$bookmark_uri/title");
+    is($res->content_type, 'text/plain', 'Title is text/plain');
+    is($res->content, 'My Other Bookmark', 'Title has correct value');
+    $res = $test->request(GET "$bookmark_uri/uri");
+    is($res->content_type, 'text/plain', 'URI is text/plain');
+    is($res->content, 'http://search.cpan.org', 'URI has correct value');
+    $res = $test->request(GET "$bookmark_uri/tags");
+    is($res->content_type, 'text/plain', 'Tags are text/plain');
+    is($res->content, 'one test two', 'Tags have correct value');
+};
+
+$res = $test->request(GET "$bookmark_uri/id");
+my $id = $res->content;
+$res = $test->request(GET "$bookmark_uri/ctime");
+my $ctime = $res->content;
+$res = $test->request(GET "$bookmark_uri/mtime");
+my $mtime = $res->content;
+
+$res = $test->request(GET "$bookmark_uri?format=json");
+is($res->content_type, 'application/json');
+is_valid_json $res->content;
+is_json $res->content, <<"END_JSON";
+{
+    "id": $id,
+    "bookmark_uri": "$bookmark_uri",
+    "ctime": $ctime,
+    "mtime": $mtime,
+    "uri": "http://search.cpan.org",
+    "title": "My Other Bookmark",
+    "tags": ["one", "test", "two"]
+}
+END_JSON
+
+done_testing();
+
+# test fixture teardown
+unlink $dbname if -e $dbname;
Index: /trunk/t/01-bookmark.t
===================================================================
--- /trunk/t/01-bookmark.t	(revision 92)
+++ /trunk/t/01-bookmark.t	(revision 92)
@@ -0,0 +1,41 @@
+#!/usr/bin/perl -w
+use strict;
+
+use Test::More;
+
+use Bookmark;
+
+subtest 'Basic bookmark' => sub {
+    my $time_1 = time - 30;
+    my $time_2 = time;
+    my $bookmark = Bookmark->new({
+        id => 1,
+        uri => 'http://metacpan.org',
+        title => 'My Bookmark',
+        ctime => $time_1,
+        mtime => $time_2,
+        tags => [qw{test}],
+    });
+
+    is($bookmark->id, 1, 'id set okay');
+    is($bookmark->uri, 'http://metacpan.org', 'uri set okay');
+    is($bookmark->title, 'My Bookmark', 'title set okay');
+    is($bookmark->ctime, $time_1, 'ctime set okay');
+    is($bookmark->mtime, $time_2, 'mtime set okay');
+    is_deeply($bookmark->tags, [qw{test}], 'tags set okay');
+};
+
+subtest 'Implicit mtime' => sub {
+    my $time = time;
+    my $bookmark = Bookmark->new({
+        id => 1,
+        uri => 'http://metacpan.org',
+        title => 'My Bookmark',
+        ctime => $time,
+        tags => [qw{test}],
+    });
+
+    is($bookmark->mtime, $time, 'implicit mtime set okay');
+};
+
+done_testing();
