blob: ddb882888daaf6532be775a62838c6cddb8aaf7e [file] [log] [blame]
James E. Blair4f568262017-12-21 09:18:21 -08001#!/usr/bin/env python
2
3# Copyright 2014 Hewlett-Packard Development Company, L.P.
4# Copyright 2014 Rackspace Australia
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
18import asyncio
19import threading
20import os
21import json
22import urllib
23import time
24import socket
James E. Blair4f568262017-12-21 09:18:21 -080025
26import zuul.web
27
28from tests.base import ZuulTestCase, FIXTURE_DIR
29
30
Monty Taylor518dcf82018-01-23 12:51:26 -060031class FakeConfig(object):
32
33 def __init__(self, config):
34 self.config = config or {}
35
36 def has_option(self, section, option):
37 return option in self.config.get(section, {})
38
39 def get(self, section, option):
40 return self.config.get(section, {}).get(option)
41
42
43class BaseTestWeb(ZuulTestCase):
James E. Blair4f568262017-12-21 09:18:21 -080044 tenant_config_file = 'config/single-tenant/main.yaml'
Monty Taylor518dcf82018-01-23 12:51:26 -060045 config_ini_data = {}
James E. Blair4f568262017-12-21 09:18:21 -080046
47 def setUp(self):
Monty Taylor4a781a72017-07-25 07:28:04 -040048 self.assertTrue(
49 os.path.exists(zuul.web.STATIC_DIR),
50 "Static web assets are missing, be sure to run 'npm run build'")
Monty Taylor518dcf82018-01-23 12:51:26 -060051 super(BaseTestWeb, self).setUp()
James E. Blair4f568262017-12-21 09:18:21 -080052 self.executor_server.hold_jobs_in_build = True
53 A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
54 A.addApproval('Code-Review', 2)
55 self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
56 B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
57 B.addApproval('Code-Review', 2)
58 self.fake_gerrit.addEvent(B.addApproval('Approved', 1))
59 self.waitUntilSettled()
60
Monty Taylor518dcf82018-01-23 12:51:26 -060061 self.zuul_ini_config = FakeConfig(self.config_ini_data)
James E. Blair4f568262017-12-21 09:18:21 -080062 # Start the web server
63 self.web = zuul.web.ZuulWeb(
64 listen_address='127.0.0.1', listen_port=0,
Monty Taylor518dcf82018-01-23 12:51:26 -060065 gear_server='127.0.0.1', gear_port=self.gearman_server.port,
66 info=zuul.model.WebInfo.fromConfig(self.zuul_ini_config)
67 )
James E. Blair4f568262017-12-21 09:18:21 -080068 loop = asyncio.new_event_loop()
69 loop.set_debug(True)
70 ws_thread = threading.Thread(target=self.web.run, args=(loop,))
71 ws_thread.start()
72 self.addCleanup(loop.close)
73 self.addCleanup(ws_thread.join)
74 self.addCleanup(self.web.stop)
75
76 self.host = 'localhost'
77 # Wait until web server is started
78 while True:
79 time.sleep(0.1)
80 if self.web.server is None:
81 continue
82 self.port = self.web.server.sockets[0].getsockname()[1]
83 print(self.host, self.port)
84 try:
85 with socket.create_connection((self.host, self.port)):
86 break
87 except ConnectionRefusedError:
88 pass
89
90 def tearDown(self):
91 self.executor_server.hold_jobs_in_build = False
92 self.executor_server.release()
93 self.waitUntilSettled()
Monty Taylor518dcf82018-01-23 12:51:26 -060094 super(BaseTestWeb, self).tearDown()
95
96
97class TestWeb(BaseTestWeb):
James E. Blair4f568262017-12-21 09:18:21 -080098
99 def test_web_status(self):
Tobias Henkele0bad8d2018-01-23 12:34:15 +0100100 "Test that we can retrieve JSON status info"
101 self.executor_server.hold_jobs_in_build = True
102 A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
103 A.addApproval('Code-Review', 2)
104 self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
105 self.waitUntilSettled()
106
107 self.executor_server.release('project-merge')
108 self.waitUntilSettled()
James E. Blair4f568262017-12-21 09:18:21 -0800109
110 req = urllib.request.Request(
Monty Taylor9010dc52018-02-17 13:29:28 -0600111 "http://localhost:%s/tenant-one/status" % self.port)
James E. Blair4f568262017-12-21 09:18:21 -0800112 f = urllib.request.urlopen(req)
Tobias Henkele0bad8d2018-01-23 12:34:15 +0100113 headers = f.info()
114 self.assertIn('Content-Length', headers)
115 self.assertIn('Content-Type', headers)
116 self.assertEqual(
117 'application/json; charset=utf-8', headers['Content-Type'])
118 self.assertIn('Access-Control-Allow-Origin', headers)
119 self.assertIn('Cache-Control', headers)
120 self.assertIn('Last-Modified', headers)
121 data = f.read().decode('utf8')
James E. Blair4f568262017-12-21 09:18:21 -0800122
Tobias Henkele0bad8d2018-01-23 12:34:15 +0100123 self.executor_server.hold_jobs_in_build = False
124 self.executor_server.release()
125 self.waitUntilSettled()
126
127 data = json.loads(data)
128 status_jobs = []
129 for p in data['pipelines']:
130 for q in p['change_queues']:
131 if p['name'] in ['gate', 'conflict']:
132 self.assertEqual(q['window'], 20)
133 else:
134 self.assertEqual(q['window'], 0)
135 for head in q['heads']:
136 for change in head:
137 self.assertTrue(change['active'])
138 self.assertIn(change['id'], ('1,1', '2,1', '3,1'))
139 for job in change['jobs']:
140 status_jobs.append(job)
141 self.assertEqual('project-merge', status_jobs[0]['name'])
142 # TODO(mordred) pull uuids from self.builds
143 self.assertEqual(
144 'stream.html?uuid={uuid}&logfile=console.log'.format(
145 uuid=status_jobs[0]['uuid']),
146 status_jobs[0]['url'])
147 self.assertEqual(
148 'finger://{hostname}/{uuid}'.format(
149 hostname=self.executor_server.hostname,
150 uuid=status_jobs[0]['uuid']),
151 status_jobs[0]['finger_url'])
152 # TOOD(mordred) configure a success-url on the base job
153 self.assertEqual(
154 'finger://{hostname}/{uuid}'.format(
155 hostname=self.executor_server.hostname,
156 uuid=status_jobs[0]['uuid']),
157 status_jobs[0]['report_url'])
158 self.assertEqual('project-test1', status_jobs[1]['name'])
159 self.assertEqual(
160 'stream.html?uuid={uuid}&logfile=console.log'.format(
161 uuid=status_jobs[1]['uuid']),
162 status_jobs[1]['url'])
163 self.assertEqual(
164 'finger://{hostname}/{uuid}'.format(
165 hostname=self.executor_server.hostname,
166 uuid=status_jobs[1]['uuid']),
167 status_jobs[1]['finger_url'])
168 self.assertEqual(
169 'finger://{hostname}/{uuid}'.format(
170 hostname=self.executor_server.hostname,
171 uuid=status_jobs[1]['uuid']),
172 status_jobs[1]['report_url'])
173
174 self.assertEqual('project-test2', status_jobs[2]['name'])
175 self.assertEqual(
176 'stream.html?uuid={uuid}&logfile=console.log'.format(
177 uuid=status_jobs[2]['uuid']),
178 status_jobs[2]['url'])
179 self.assertEqual(
180 'finger://{hostname}/{uuid}'.format(
181 hostname=self.executor_server.hostname,
182 uuid=status_jobs[2]['uuid']),
183 status_jobs[2]['finger_url'])
184 self.assertEqual(
185 'finger://{hostname}/{uuid}'.format(
186 hostname=self.executor_server.hostname,
187 uuid=status_jobs[2]['uuid']),
188 status_jobs[2]['report_url'])
189
190 # check job dependencies
191 self.assertIsNotNone(status_jobs[0]['dependencies'])
192 self.assertIsNotNone(status_jobs[1]['dependencies'])
193 self.assertIsNotNone(status_jobs[2]['dependencies'])
194 self.assertEqual(len(status_jobs[0]['dependencies']), 0)
195 self.assertEqual(len(status_jobs[1]['dependencies']), 1)
196 self.assertEqual(len(status_jobs[2]['dependencies']), 1)
197 self.assertIn('project-merge', status_jobs[1]['dependencies'])
198 self.assertIn('project-merge', status_jobs[2]['dependencies'])
James E. Blair4f568262017-12-21 09:18:21 -0800199
200 def test_web_bad_url(self):
201 # do we 404 correctly
202 req = urllib.request.Request(
203 "http://localhost:%s/status/foo" % self.port)
204 self.assertRaises(urllib.error.HTTPError, urllib.request.urlopen, req)
205
James E. Blair4f568262017-12-21 09:18:21 -0800206 def test_web_find_change(self):
207 # can we filter by change id
208 req = urllib.request.Request(
209 "http://localhost:%s/tenant-one/status/change/1,1" % self.port)
210 f = urllib.request.urlopen(req)
211 data = json.loads(f.read().decode('utf8'))
212
213 self.assertEqual(1, len(data), data)
214 self.assertEqual("org/project", data[0]['project'])
215
216 req = urllib.request.Request(
217 "http://localhost:%s/tenant-one/status/change/2,1" % self.port)
218 f = urllib.request.urlopen(req)
219 data = json.loads(f.read().decode('utf8'))
220
221 self.assertEqual(1, len(data), data)
222 self.assertEqual("org/project1", data[0]['project'], data)
223
224 def test_web_keys(self):
225 with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f:
226 public_pem = f.read()
227
228 req = urllib.request.Request(
229 "http://localhost:%s/tenant-one/org/project.pub" %
230 self.port)
231 f = urllib.request.urlopen(req)
232 self.assertEqual(f.read(), public_pem)
233
James E. Blair4f568262017-12-21 09:18:21 -0800234 def test_web_404_on_unknown_tenant(self):
235 req = urllib.request.Request(
Monty Taylor9010dc52018-02-17 13:29:28 -0600236 "http://localhost:{}/non-tenant/status".format(self.port))
James E. Blair4f568262017-12-21 09:18:21 -0800237 e = self.assertRaises(
238 urllib.error.HTTPError, urllib.request.urlopen, req)
239 self.assertEqual(404, e.code)
Monty Taylor518dcf82018-01-23 12:51:26 -0600240
241
242class TestInfo(BaseTestWeb):
243
244 def setUp(self):
245 super(TestInfo, self).setUp()
246 web_config = self.config_ini_data.get('web', {})
247 self.websocket_url = web_config.get('websocket_url')
248 self.stats_url = web_config.get('stats_url')
249 statsd_config = self.config_ini_data.get('statsd', {})
250 self.stats_prefix = statsd_config.get('prefix')
251
252 def test_info(self):
253 req = urllib.request.Request(
254 "http://localhost:%s/info" % self.port)
255 f = urllib.request.urlopen(req)
256 info = json.loads(f.read().decode('utf8'))
257 self.assertEqual(
258 info, {
259 "info": {
Monty Taylorf93c2fb2018-02-20 14:38:47 -0600260 "rest_api_url": None,
Monty Taylor518dcf82018-01-23 12:51:26 -0600261 "capabilities": {
262 "job_history": False
263 },
264 "stats": {
265 "url": self.stats_url,
266 "prefix": self.stats_prefix,
267 "type": "graphite",
268 },
269 "websocket_url": self.websocket_url,
270 }
271 })
272
273 def test_tenant_info(self):
274 req = urllib.request.Request(
275 "http://localhost:%s/tenant-one/info" % self.port)
276 f = urllib.request.urlopen(req)
277 info = json.loads(f.read().decode('utf8'))
278 self.assertEqual(
279 info, {
280 "info": {
Monty Taylorf93c2fb2018-02-20 14:38:47 -0600281 "rest_api_url": None,
Monty Taylor518dcf82018-01-23 12:51:26 -0600282 "tenant": "tenant-one",
283 "capabilities": {
284 "job_history": False
285 },
286 "stats": {
287 "url": self.stats_url,
288 "prefix": self.stats_prefix,
289 "type": "graphite",
290 },
291 "websocket_url": self.websocket_url,
292 }
293 })
294
295
296class TestWebSocketInfo(TestInfo):
297
298 config_ini_data = {
299 'web': {
300 'websocket_url': 'wss://ws.example.com'
301 }
302 }
303
304
305class TestGraphiteUrl(TestInfo):
306
307 config_ini_data = {
308 'statsd': {
309 'prefix': 'example'
310 },
311 'web': {
312 'stats_url': 'https://graphite.example.com',
313 }
314 }