[codesign] add function to check notarization status (#2107)

diff --git a/codesign/lib/src/file_codesign_visitor.dart b/codesign/lib/src/file_codesign_visitor.dart
index 486c918..7ebc23d 100644
--- a/codesign/lib/src/file_codesign_visitor.dart
+++ b/codesign/lib/src/file_codesign_visitor.dart
@@ -90,6 +90,8 @@
     </dict>
 </plist>
 ''';
+  static const Duration _notarizationTimerDuration = Duration(seconds: 45);
+  static final RegExp _notarytoolStatusCheckPattern = RegExp(r'[ ]*status: ([a-zA-z ]+)');
 
   static const String fixItInstructions = '''
 Codesign test failed.
@@ -245,4 +247,76 @@
     fileWithEntitlements.addAll(await fileSystem.file(entitlementFilePath).readAsLines());
     return fileWithEntitlements;
   }
+
+  /// Upload a zip archive to the notary service and verify the build succeeded.
+  ///
+  /// The apple notarization service will unzip the artifact, validate all
+  /// binaries are properly codesigned, and notarize the entire archive.
+  Future<void> notarize(File file) async {
+    final Completer<void> completer = Completer<void>();
+    final String uuid = uploadZipToNotary(file);
+
+    Future<void> callback(Timer timer) async {
+      final bool notaryFinished = checkNotaryJobFinished(uuid);
+      if (notaryFinished) {
+        timer.cancel();
+        log.info('successfully notarized ${file.path}');
+        completer.complete();
+      }
+    }
+
+    // check on results
+    Timer.periodic(
+      _notarizationTimerDuration,
+      callback,
+    );
+    await completer.future;
+  }
+
+  String uploadZipToNotary(File localFile, [int retryCount = 3]) {
+    throw UnimplementedError('will implement later');
+  }
+
+  /// Make a request to the notary service to see if the notary job is finished.
+  ///
+  /// A return value of true means that notarization finished successfully,
+  /// false means that the job is still pending. If the notarization fails, this
+  /// function will throw a [ConductorException].
+  bool checkNotaryJobFinished(String uuid) {
+    final List<String> args = <String>[
+      'xcrun',
+      'notarytool',
+      'info',
+      uuid,
+      '--password',
+      appSpecificPassword,
+      '--apple-id',
+      codesignAppstoreId,
+      '--team-id',
+      codesignTeamId,
+    ];
+
+    log.info('checking notary status with ${args.join(' ')}');
+    final io.ProcessResult result = processManager.runSync(args);
+    final String combinedOutput = (result.stdout as String) + (result.stderr as String);
+
+    final RegExpMatch? match = _notarytoolStatusCheckPattern.firstMatch(combinedOutput);
+
+    if (match == null) {
+      throw CodesignException(
+        'Malformed output from "${args.join(' ')}"\n${combinedOutput.trim()}',
+      );
+    }
+
+    final String status = match.group(1)!;
+
+    if (status == 'Accepted') {
+      return true;
+    }
+    if (status == 'In Progress') {
+      log.info('job $uuid still pending');
+      return false;
+    }
+    throw CodesignException('Notarization failed with: $status\n$combinedOutput');
+  }
 }
diff --git a/codesign/test/file_codesign_visitor_test.dart b/codesign/test/file_codesign_visitor_test.dart
index 5ee2a79..4890a24 100644
--- a/codesign/test/file_codesign_visitor_test.dart
+++ b/codesign/test/file_codesign_visitor_test.dart
@@ -460,4 +460,144 @@
           ));
     });
   });
+
+  group('notarization tests: ', () {
+    setUp(() {
+      tempDir = fileSystem.systemTempDirectory.createTempSync('conductor_codesign');
+      processManager = FakeProcessManager.list(<FakeCommand>[]);
+      codesignVisitor = cs.FileCodesignVisitor(
+        codesignCertName: randomString,
+        codesignUserName: randomString,
+        appSpecificPassword: randomString,
+        codesignAppstoreId: randomString,
+        codesignTeamId: randomString,
+        codesignFilepaths: fakeFilepaths,
+        commitHash: randomString,
+        fileSystem: fileSystem,
+        processManager: processManager,
+        tempDir: tempDir,
+      );
+      codesignVisitor.directoriesVisited.clear();
+      records.clear();
+      log.onRecord.listen((LogRecord record) => records.add(record));
+    });
+
+    test('successful notarization check returns true', () async {
+      processManager.addCommands(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'info',
+            randomString,
+            '--password',
+            randomString,
+            '--apple-id',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''createdDate: 2021-04-29T01:38:09.498Z
+id: 2efe2717-52ef-43a5-96dc-0797e4ca1041
+name: OvernightTextEditor_11.6.8.zip
+status: Accepted''',
+        ),
+      ]);
+
+      expect(
+        codesignVisitor.checkNotaryJobFinished(randomString),
+        true,
+      );
+    });
+
+    test('wrong format (such as altool) check throws exception', () async {
+      processManager.addCommands(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'info',
+            randomString,
+            '--password',
+            randomString,
+            '--apple-id',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''RequestUUID: 2EFE2717-52EF-43A5-96DC-0797E4CA1041
+Date: 2021-07-02 20:32:01 +0000
+Status: invalid
+LogFileURL: https://osxapps.itunes.apple.com/...
+Status Code: 2
+Status Message: Package Invalid''',
+        ),
+      ]);
+
+      expect(
+        () => codesignVisitor.checkNotaryJobFinished(randomString),
+        throwsA(
+          isA<CodesignException>(),
+        ),
+      );
+    });
+
+    test('in progress notarization check returns false', () async {
+      processManager.addCommands(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'info',
+            randomString,
+            '--password',
+            randomString,
+            '--apple-id',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''createdDate: 2021-04-29T01:38:09.498Z
+id: 2efe2717-52ef-43a5-96dc-0797e4ca1041
+name: OvernightTextEditor_11.6.8.zip
+status: In Progress''',
+        ),
+      ]);
+
+      expect(
+        codesignVisitor.checkNotaryJobFinished(randomString),
+        false,
+      );
+    });
+
+    test('invalid status check throws exception', () async {
+      processManager.addCommands(<FakeCommand>[
+        const FakeCommand(
+          command: <String>[
+            'xcrun',
+            'notarytool',
+            'info',
+            randomString,
+            '--password',
+            randomString,
+            '--apple-id',
+            randomString,
+            '--team-id',
+            randomString,
+          ],
+          stdout: '''createdDate: 2021-04-29T01:38:09.498Z
+id: 2efe2717-52ef-43a5-96dc-0797e4ca1041
+name: OvernightTextEditor_11.6.8.zip
+status: Invalid''',
+        ),
+      ]);
+
+      expect(
+        () => codesignVisitor.checkNotaryJobFinished(randomString),
+        throwsA(
+          isA<CodesignException>(),
+        ),
+      );
+    });
+  });
 }