Handle sub-second resolution in DateTime vs second-resolution in HTTP date
diff --git a/lib/shelf_static.dart b/lib/shelf_static.dart
index ebdea75..a2eba04 100644
--- a/lib/shelf_static.dart
+++ b/lib/shelf_static.dart
@@ -7,6 +7,8 @@
 import 'package:path/path.dart' as p;
 import 'package:shelf/shelf.dart';
 
+import 'src/util.dart';
+
 // directory listing
 // hidden files
 
@@ -87,8 +89,12 @@
 
     var ifModifiedSince = request.ifModifiedSince;
 
-    if (ifModifiedSince != null && !fileStat.changed.isAfter(ifModifiedSince)) {
-      return new Response.notModified();
+
+    if (ifModifiedSince != null) {
+      var fileChangeAtSecResolution = toSecondResolution(fileStat.changed);
+      if (!fileChangeAtSecResolution.isAfter(ifModifiedSince)) {
+        return new Response.notModified();
+      }
     }
 
     var headers = <String, String>{
diff --git a/lib/src/util.dart b/lib/src/util.dart
index 7a305da..62dbf47 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -10,3 +10,8 @@
 
 /// Like [Future.sync], but wraps the Future in [Chain.track] as well.
 Future syncFuture(callback()) => Chain.track(new Future.sync(callback));
+
+DateTime toSecondResolution(DateTime dt) {
+  if (dt.millisecond == 0) return dt;
+  return dt.subtract(new Duration(milliseconds: dt.millisecond));
+}
diff --git a/test/basic_file_test.dart b/test/basic_file_test.dart
index 7d8c953..c128202 100644
--- a/test/basic_file_test.dart
+++ b/test/basic_file_test.dart
@@ -97,7 +97,7 @@
       var modified = new File(rootPath).statSync().changed.toUtc();
 
       return makeRequest(handler, '/root.txt').then((response) {
-        expect(response.lastModified, modified);
+        expect(response.lastModified, atSameTimeToSecond(modified));
       });
     });
   });
@@ -137,7 +137,7 @@
         return makeRequest(handler, '/root.txt', headers: headers)
             .then((response) {
           expect(response.statusCode, HttpStatus.OK);
-          expect(response.lastModified, modified);
+          expect(response.lastModified, atSameTimeToSecond(modified));
         });
       });
     });
diff --git a/test/sample_test.dart b/test/sample_test.dart
index c939471..0f26a5a 100644
--- a/test/sample_test.dart
+++ b/test/sample_test.dart
@@ -2,6 +2,7 @@
 
 import 'dart:async';
 import 'dart:io';
+
 import 'package:path/path.dart' as p;
 import 'package:scheduled_test/scheduled_test.dart';
 
@@ -9,6 +10,8 @@
 import 'package:shelf_static/shelf_static.dart';
 import 'package:shelf_static/src/util.dart';
 
+import 'test_util.dart';
+
 void main() {
   group('/index.html', () {
     test('body is correct', () {
@@ -61,7 +64,7 @@
 
   return _requestFile(filename).then((response) {
     expect(response.contentLength, fileStat.size);
-    expect(response.lastModified, fileStat.changed.toUtc());
+    expect(response.lastModified, atSameTimeToSecond(fileStat.changed.toUtc()));
     return _expectCompletesWithBytes(response, fileContents);
   });
 }
diff --git a/test/test_util.dart b/test/test_util.dart
index 58fa18a..cbd7544 100644
--- a/test/test_util.dart
+++ b/test/test_util.dart
@@ -6,6 +6,7 @@
 
 import 'dart:async';
 
+import 'package:matcher/matcher.dart';
 import 'package:path/path.dart' as p;
 import 'package:shelf/shelf.dart';
 import 'package:shelf_static/src/util.dart';
@@ -42,3 +43,27 @@
     return handler(relativeRequest);
   };
 }
+
+Matcher atSameTimeToSecond(value) =>
+    new _SecondResolutionDateTimeMatcher(value);
+
+class _SecondResolutionDateTimeMatcher extends Matcher {
+  final DateTime _target;
+
+  _SecondResolutionDateTimeMatcher(DateTime target) :
+    this._target = toSecondResolution(target);
+
+  bool matches(item, Map matchState) {
+    if (item is! DateTime) return false;
+
+    return datesEqualToSecond(_target, item);
+  }
+
+  Description describe(Description descirption) =>
+      descirption.add('Must be at the same moment as $_target with resolution '
+          'to the second.');
+}
+
+bool datesEqualToSecond(DateTime d1, DateTime d2) {
+  return toSecondResolution(d1).isAtSameMomentAs(toSecondResolution(d2));
+}