[tests] Initial set of tests for the doc_checker lib code.

Change-Id: I94bea2f43ca1424201412f0a3c50f07fe30ca468
diff --git a/packages/tools/BUILD.gn b/packages/tools/BUILD.gn
index b513b5a..27ac4b0 100644
--- a/packages/tools/BUILD.gn
+++ b/packages/tools/BUILD.gn
@@ -28,5 +28,6 @@
   testonly = true
   public_deps = [
     "//topaz/tools/doc_checker(//build/toolchain:host_x64)",
+    "//topaz/tools/doc_checker:tests(//build/toolchain:host_x64)",
   ]
 }
diff --git a/tools/doc_checker/BUILD.gn b/tools/doc_checker/BUILD.gn
index 30c3713..71b75fe 100644
--- a/tools/doc_checker/BUILD.gn
+++ b/tools/doc_checker/BUILD.gn
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//build/dart/dart_tool.gni")
+import("//build/dart/test.gni")
 
 dart_tool("doc_checker") {
   package_name = "doc_checker"
@@ -23,3 +24,25 @@
     "//third_party/dart-pkg/pub/path",
   ]
 }
+
+dart_test("doc_checker_tests") {
+  sources = [
+    "graph_test.dart",
+    "link_scraper_test.dart",
+    "link_verifier_test.dart",
+  ]
+
+  deps = [
+    ":doc_checker_dart_library",
+    "//third_party/dart-pkg/pub/meta",
+    "//third_party/dart-pkg/pub/test",
+  ]
+}
+
+group("tests") {
+  testonly = true
+
+  deps = [
+    ":doc_checker_tests($host_toolchain)",
+  ]
+}
diff --git a/tools/doc_checker/lib/graph.dart b/tools/doc_checker/lib/graph.dart
index 9936c11..67a8bff 100644
--- a/tools/doc_checker/lib/graph.dart
+++ b/tools/doc_checker/lib/graph.dart
@@ -13,6 +13,8 @@
   int _nextId = 0;
   Node _root;
 
+  int get nodeCount => _nodes.length;
+
   /// Returns or creates a node with the given [label].
   Node getNode(String label) =>
       _nodes.putIfAbsent(label, () => Node._internal(label, _nextId++));
diff --git a/tools/doc_checker/lib/link_scraper.dart b/tools/doc_checker/lib/link_scraper.dart
index c5150dd..c491a50 100644
--- a/tools/doc_checker/lib/link_scraper.dart
+++ b/tools/doc_checker/lib/link_scraper.dart
@@ -5,13 +5,19 @@
 import 'dart:io';
 
 import 'package:markdown/markdown.dart';
+import 'package:meta/meta.dart';
 
 /// Scrapes links in a markdown document.
 class LinkScraper {
   /// Extracts links from the given [file].
   Iterable<String> scrape(String file) {
-    final List<Node> nodes =
-        Document().parseLines(File(file).readAsLinesSync());
+    return scrapeLines(File(file).readAsLinesSync());
+  }
+
+  /// Extracts links from the given list of [lines].
+  @visibleForTesting
+  Iterable<String> scrapeLines(List<String> lines) {
+    final List<Node> nodes = Document().parseLines(lines);
     final _Visitor visitor = _Visitor();
     for (Node node in nodes) {
       node.accept(visitor);
diff --git a/tools/doc_checker/lib/link_verifier.dart b/tools/doc_checker/lib/link_verifier.dart
index 91a84f0..da341a0 100644
--- a/tools/doc_checker/lib/link_verifier.dart
+++ b/tools/doc_checker/lib/link_verifier.dart
@@ -6,6 +6,7 @@
 import 'dart:io';
 
 import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
 
 class Link<P> {
   final Uri uri;
@@ -28,28 +29,31 @@
     urisByDomain.putIfAbsent(link.uri.authority, () => []).add(link);
   }
   await Future.wait(urisByDomain.keys.map((String domain) =>
-      _LinkVerifier(urisByDomain[domain]).verify(callback)));
+      LinkVerifier(urisByDomain[domain], http.Client()).verify(callback)));
   return null;
 }
 
-class _LinkVerifier<P> {
+@visibleForTesting
+class LinkVerifier<P> {
   final List<Link<P>> links;
+  final http.Client client;
 
-  _LinkVerifier(this.links);
+  LinkVerifier(this.links, this.client);
 
   Future<Null> verify(OnElementVerified<P> callback) async {
     for (Link<P> link in links) {
-      callback(link, await _verifyLink(link));
+      callback(link, await verifyLink(link));
     }
     return null;
   }
 
-  Future<bool> _verifyLink(Link<P> link) async {
+  @visibleForTesting
+  Future<bool> verifyLink(Link<P> link) async {
     try {
       for (int i = 0; i < 3; i++) {
-        final http.Response response = await http.get(link.uri, headers: {
-            HttpHeaders.acceptHeader:
-                'text/html,application/xhtml+xml,application/xml,',
+        final http.Response response = await client.get(link.uri, headers: {
+          HttpHeaders.acceptHeader:
+              'text/html,application/xhtml+xml,application/xml,',
         });
         final int code = response.statusCode;
         if (code == HttpStatus.tooManyRequests) {
@@ -62,8 +66,9 @@
         // Http client doesn't automatically follow 308 (Permanent Redirect).
         if (code == HttpStatus.permanentRedirect) {
           if (response.headers.containsKey('location')) {
-            Uri redirectUri = Uri.parse(link.uri.origin + response.headers['location']);
-            return _verifyLink(Link<P>(redirectUri, link.payload));
+            Uri redirectUri =
+                Uri.parse(link.uri.origin + response.headers['location']);
+            return verifyLink(Link<P>(redirectUri, link.payload));
           }
           return false;
         }
diff --git a/tools/doc_checker/test/graph_test.dart b/tools/doc_checker/test/graph_test.dart
new file mode 100644
index 0000000..70734ac
--- /dev/null
+++ b/tools/doc_checker/test/graph_test.dart
@@ -0,0 +1,43 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:doc_checker/graph.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('doc_checker graph tests', () {
+    test('getNode adds new node to graph', () {
+      final Graph graph = Graph();
+      expect(graph.nodeCount, equals(0));
+      graph.getNode('label');
+      expect(graph.nodeCount, equals(1));
+    });
+
+    test('getNode returns existing node', () {
+      final Graph graph = Graph();
+      final Node node1 = graph.getNode('label');
+      final Node node2 = graph.getNode('label');
+      expect(graph.nodeCount, equals(1));
+      expect(node1, equals(node2));
+    });
+
+    test('no orphans with node connected to root', () {
+      final Graph graph = Graph();
+      final Node root = graph.getNode('root');
+      graph.root = root;
+      final Node node = graph.getNode('label');
+      graph.addEdge(from: root, to: node);
+      expect(graph.orphans, hasLength(0));
+    });
+
+    test('unknown node cannot be root', () {
+      final Graph graph = Graph();
+      final Node unknown = graph.getNode('unknown');
+      expect(graph.nodeCount, equals(1));
+      graph.removeSingletons();
+      expect(graph.nodeCount, equals(0));
+      expect(() => graph.root = unknown, throwsException);
+    });
+  });
+}
diff --git a/tools/doc_checker/test/link_scraper_test.dart b/tools/doc_checker/test/link_scraper_test.dart
new file mode 100644
index 0000000..bb431d2
--- /dev/null
+++ b/tools/doc_checker/test/link_scraper_test.dart
@@ -0,0 +1,26 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:doc_checker/link_scraper.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('doc_checker link_scraper tests', () {
+    test('no links found in plain text', () {
+      final List<String> lines = ['this text has no links', 'same here'];
+      expect(LinkScraper().scrapeLines(lines).isEmpty, isTrue);
+    });
+
+    test('links get scraped', () {
+      final List<String> lines = [
+        'this text has no links',
+        'this one [does](link.md).',
+        'but not *this one*'
+      ];
+      Iterable<String> links = LinkScraper().scrapeLines(lines);
+      expect(links, hasLength(1));
+      expect(links.first, equals('link.md'));
+    });
+  });
+}
diff --git a/tools/doc_checker/test/link_verifier_test.dart b/tools/doc_checker/test/link_verifier_test.dart
new file mode 100644
index 0000000..2fd1b74
--- /dev/null
+++ b/tools/doc_checker/test/link_verifier_test.dart
@@ -0,0 +1,63 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:doc_checker/link_verifier.dart';
+import 'package:http/http.dart';
+import 'package:http/testing.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('doc_checker link_verifier tests', () {
+    test('link to string conversion returns the Uri', () {
+      Link<String> link = Link(Uri.parse('https://www.example.com'), 'label');
+      expect(link.toString(), equals('https://www.example.com'));
+    });
+
+    test('response code 200 is considered valid', () async {
+      Client client = MockClient((request) async {
+        return Response('', HttpStatus.ok);
+      });
+      Link<String> link = Link(Uri.parse('https://www.example.com'), 'label');
+      List<Link<String>> links = <Link<String>>[];
+
+      LinkVerifier<String> linkVerifier = LinkVerifier(links, client);
+      expect(linkVerifier.verifyLink(link), completion(isTrue));
+    });
+
+    test('response code 404 is considered invalid', () async {
+      Client client = MockClient((request) async {
+        return Response('', HttpStatus.notFound);
+      });
+      Link<String> link = Link(Uri.parse('https://www.example.com'), 'label');
+      List<Link<String>> links = <Link<String>>[];
+
+      LinkVerifier<String> linkVerifier = LinkVerifier(links, client);
+      expect(linkVerifier.verifyLink(link), completion(isFalse));
+    });
+
+    test('link verifier follows redirect', () async {
+      final Uri srcUri = Uri.parse('https://www.redirect.com');
+      final Uri destUri = Uri.parse('https://www.redirect.com/newpage');
+      Client client = MockClient((request) async {
+        if (request.url == srcUri) {
+          return Response('', HttpStatus.permanentRedirect,
+              headers: {'location': '/newpage'});
+        } else if (request.url == destUri) {
+          return Response('', HttpStatus.ok);
+        } else {
+          return Response('', HttpStatus.notFound);
+        }
+      });
+
+      Link<String> link = Link(srcUri, 'label');
+      List<Link<String>> links = <Link<String>>[];
+
+      LinkVerifier<String> linkVerifier = LinkVerifier(links, client);
+      var valid = await linkVerifier.verifyLink(link);
+      expect(valid, isTrue);
+    });
+  });
+}