Require a base job

This makes base jobs required and allows for a per-tenant default.

Story: 2001110
Task: 4793
Change-Id: I26ffddad8358c156cfac749ce98af70f3447f671
diff --git a/tests/unit/test_configloader.py b/tests/unit/test_configloader.py
index 1ba4ed9..3b5c206 100644
--- a/tests/unit/test_configloader.py
+++ b/tests/unit/test_configloader.py
@@ -305,5 +305,6 @@
         tenant = self.sched.abide.tenants.get('tenant-one')
         jobs = sorted(tenant.layout.jobs.keys())
         self.assertEquals(
-            ['noop', 'trusted-zuul.yaml-job', 'untrusted-zuul.yaml-job'],
+            ['base', 'noop', 'trusted-zuul.yaml-job',
+             'untrusted-zuul.yaml-job'],
             jobs)
diff --git a/tests/unit/test_model.py b/tests/unit/test_model.py
index 3538555..6a63125 100644
--- a/tests/unit/test_model.py
+++ b/tests/unit/test_model.py
@@ -65,6 +65,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'job',
+            'parent': None,
             'irrelevant-files': [
                 '^docs/.*$'
             ]})
@@ -184,6 +185,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'base',
+            'parent': None,
             'timeout': 30,
             'pre-run': 'base-pre',
             'post-run': 'base-post',
@@ -389,6 +391,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'base',
+            'parent': None,
             'timeout': 30,
         })
         layout.addJob(base)
@@ -487,6 +490,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'base',
+            'parent': None,
             'timeout': 30,
         })
         layout.addJob(base)
@@ -565,6 +569,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'base',
+            'parent': None,
             'timeout': 30,
         })
         layout.addJob(base)
@@ -614,6 +619,7 @@
         base = configloader.JobParser.fromYaml(tenant, layout, {
             '_source_context': base_context,
             '_start_mark': self.start_mark,
+            'parent': None,
             'name': 'base',
         })
         layout.addJob(base)
@@ -639,6 +645,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'job',
+            'parent': None,
             'allowed-projects': ['project'],
         })
         self.layout.addJob(job)
@@ -679,6 +686,7 @@
             '_source_context': self.context,
             '_start_mark': self.start_mark,
             'name': 'job',
+            'parent': None,
         })
         auth = model.AuthContext()
         auth.secrets.append('foo')
diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py
index 7038471..15cb561 100755
--- a/tests/unit/test_v3.py
+++ b/tests/unit/test_v3.py
@@ -1120,3 +1120,43 @@
         self.waitUntilSettled()
         self.assertNotIn("exceeds tenant max-nodes", B.messages[0],
                          "B should not fail because of nodes limit")
+
+
+class TestBaseJobs(ZuulTestCase):
+    tenant_config_file = 'config/base-jobs/main.yaml'
+
+    def test_multiple_base_jobs(self):
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertHistory([
+            dict(name='my-job', result='SUCCESS', changes='1,1'),
+            dict(name='other-job', result='SUCCESS', changes='1,1'),
+        ], ordered=False)
+        self.assertEqual(self.getJobFromHistory('my-job').
+                         parameters['zuul']['jobtags'],
+                         ['mybase'])
+        self.assertEqual(self.getJobFromHistory('other-job').
+                         parameters['zuul']['jobtags'],
+                         ['otherbase'])
+
+    def test_untrusted_base_job(self):
+        """Test that a base job may not be defined in an untrusted repo"""
+        in_repo_conf = textwrap.dedent(
+            """
+            - job:
+                name: fail-base
+                parent: null
+            """)
+
+        file_dict = {'.zuul.yaml': in_repo_conf}
+        A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
+                                           files=file_dict)
+        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
+        self.waitUntilSettled()
+        self.assertEqual(A.reported, 1,
+                         "A should report failure")
+        self.assertEqual(A.patchsets[0]['approvals'][0]['value'], "-1")
+        self.assertIn('Base jobs must be defined in config projects',
+                      A.messages[0])
+        self.assertHistory([])