add unittests of project & refactor response
[releng.git] / utils / test / result_collection_api / resources / handlers.py
1 ##############################################################################
2 # Copyright (c) 2015 Orange
3 # guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 # feng.xiaowei@zte.com.cn refactor db.pod to db.pods         5-19-2016
9 # feng.xiaowei@zte.com.cn refactor test_project to project   5-19-2016
10 # feng.xiaowei@zte.com.cn refactor response body             5-19-2016
11 # feng.xiaowei@zte.com.cn refactor pod/project response info 5-19-2016
12 ##############################################################################
13
14 import json
15
16 from tornado.web import RequestHandler, asynchronous, HTTPError
17 from tornado import gen
18 from datetime import datetime, timedelta
19
20 from models import TestCase, TestResult, CreateResponse
21 from resources.project_models import Project
22 from resources.pod_models import Pod
23 from common.constants import DEFAULT_REPRESENTATION, HTTP_BAD_REQUEST, \
24     HTTP_NOT_FOUND, HTTP_FORBIDDEN
25 from common.config import prepare_put_request
26
27 from dashboard.dashboard_utils import get_dashboard_cases, \
28     check_dashboard_ready_project, check_dashboard_ready_case, \
29     get_dashboard_result
30
31
32 def format_data(data, cls):
33     cls_data = cls.from_dict(data)
34     return cls_data.format_http()
35
36
37 class GenericApiHandler(RequestHandler):
38     """
39     The purpose of this class is to take benefit of inheritance and prepare
40     a set of common functions for
41     the handlers
42     """
43
44     def initialize(self):
45         """ Prepares the database for the entire class """
46         self.db = self.settings["db"]
47
48     def prepare(self):
49         if not (self.request.method == "GET" or self.request.method == "DELETE"):
50             if self.request.headers.get("Content-Type") is not None:
51                 if self.request.headers["Content-Type"].startswith(
52                         DEFAULT_REPRESENTATION):
53                     try:
54                         self.json_args = json.loads(self.request.body)
55                     except (ValueError, KeyError, TypeError) as error:
56                         raise HTTPError(HTTP_BAD_REQUEST,
57                                         "Bad Json format [{}]".
58                                         format(error))
59                 else:
60                     self.json_args = None
61
62     def finish_request(self, json_object=None):
63         if json_object:
64             self.write(json.dumps(json_object))
65         self.set_header("Content-Type", DEFAULT_REPRESENTATION)
66         self.finish()
67
68
69 class VersionHandler(RequestHandler):
70     """ Display a message for the API version """
71     def get(self):
72         self.write("Collection of test result API, v1")
73
74
75 class PodHandler(GenericApiHandler):
76     """ Handle the requests about the POD Platforms
77     HTTP Methdods :
78         - GET : Get PODS
79         - POST : Create a pod
80         - DELETE : DELETE POD
81     """
82
83     def initialize(self):
84         """ Prepares the database for the entire class """
85         super(PodHandler, self).initialize()
86
87     @asynchronous
88     @gen.coroutine
89     def get(self, pod_name=None):
90         """
91         Get all pods or a single pod
92         :param pod_id:
93         """
94         get_request = dict()
95
96         if pod_name is not None:
97             get_request["name"] = pod_name
98             answer = yield self.db.pods.find_one(get_request)
99             if answer is None:
100                 raise HTTPError(HTTP_NOT_FOUND,
101                                 "{} Not Exist".format(pod_name))
102             else:
103                 answer = format_data(answer, Pod)
104         else:
105             res = []
106             cursor = self.db.pods.find(get_request)
107             while (yield cursor.fetch_next):
108                 res.append(format_data(cursor.next_object(), Pod))
109             answer = dict()
110             answer['pods'] = res
111
112         self.finish_request(answer)
113
114     @asynchronous
115     @gen.coroutine
116     def post(self):
117         """ Create a POD"""
118
119         if self.json_args is None:
120             raise HTTPError(HTTP_BAD_REQUEST)
121
122         query = {"name": self.json_args.get("name")}
123
124         # check for existing name in db
125         mongo_dict = yield self.db.pods.find_one(query)
126         if mongo_dict is not None:
127             raise HTTPError(HTTP_FORBIDDEN,
128                             "{} already exists as a pod".format(
129                                 self.json_args.get("name")))
130
131         pod = Pod.from_dict(self.json_args)
132         pod.creation_date = datetime.now()
133
134         yield self.db.pods.insert(pod.format())
135
136         res = CreateResponse(self.request.full_url() + '/{}'.format(pod.name))
137         self.finish_request(res.format())
138
139     @asynchronous
140     @gen.coroutine
141     def delete(self, pod_name):
142         """ Remove a POD
143
144         # check for an existing pod to be deleted
145         mongo_dict = yield self.db.pods.find_one(
146             {'name': pod_name})
147         pod = TestProject.pod(mongo_dict)
148         if pod is None:
149             raise HTTPError(HTTP_NOT_FOUND,
150                             "{} could not be found as a pod to be deleted"
151                             .format(pod_name))
152
153         # just delete it, or maybe save it elsewhere in a future
154         res = yield self.db.projects.remove(
155             {'name': pod_name})
156
157         meta = dict()
158         meta["success"] = True
159         meta["deletion-data"] = res
160
161         answer = dict()
162         answer["meta"] = meta
163
164         self.finish_request(answer)
165         """
166         pass
167
168
169 class TestProjectHandler(GenericApiHandler):
170     """
171     TestProjectHandler Class
172     Handle the requests about the Test projects
173     HTTP Methdods :
174         - GET : Get all test projects and details about a specific one
175         - POST : Add a test project
176         - PUT : Edit test projects information (name and/or description)
177         - DELETE : Remove a test project
178     """
179
180     def initialize(self):
181         """ Prepares the database for the entire class """
182         super(TestProjectHandler, self).initialize()
183
184     @asynchronous
185     @gen.coroutine
186     def get(self, project_name=None):
187         """
188         Get Project(s) info
189         :param project_name:
190         """
191
192         get_request = dict()
193
194         if project_name is not None:
195             get_request["name"] = project_name
196             answer = yield self.db.projects.find_one(get_request)
197             if answer is None:
198                 raise HTTPError(HTTP_NOT_FOUND,
199                                 "{} Not Exist".format(project_name))
200             else:
201                 answer = format_data(answer, Project)
202         else:
203             res = []
204             cursor = self.db.projects.find(get_request)
205             while (yield cursor.fetch_next):
206                 res.append(format_data(cursor.next_object(), Project))
207             answer = dict()
208             answer['projects'] = res
209
210         self.finish_request(answer)
211
212     @asynchronous
213     @gen.coroutine
214     def post(self):
215         """ Create a test project"""
216
217         if self.json_args is None:
218             raise HTTPError(HTTP_BAD_REQUEST)
219
220         query = {"name": self.json_args.get("name")}
221
222         # check for name in db
223         mongo_dict = yield self.db.projects.find_one(query)
224         if mongo_dict is not None:
225             raise HTTPError(HTTP_FORBIDDEN,
226                             "{} already exists as a project".format(
227                                 self.json_args.get("name")))
228
229         project = Project.from_dict(self.json_args)
230         project.creation_date = datetime.now()
231
232         yield self.db.projects.insert(project.format())
233
234         res = CreateResponse(self.request.full_url() + '/{}'.format(project.name))
235         self.finish_request(res.format())
236
237
238     @asynchronous
239     @gen.coroutine
240     def put(self, project_name):
241         """ Updates the name and description of a test project"""
242
243         print "PUT request for : {}".format(project_name)
244
245         if self.json_args is None:
246             raise HTTPError(HTTP_BAD_REQUEST)
247
248         query = {'name': project_name}
249         mongo_dict = yield self.db.projects.find_one(query)
250         project = Project.from_dict(mongo_dict)
251         if project is None:
252             raise HTTPError(HTTP_NOT_FOUND,
253                             "{} could not be found".format(project_name))
254
255         new_name = self.json_args.get("name")
256         new_description = self.json_args.get("description")
257
258         # check for payload name parameter in db
259         # avoid a request if the project name has not changed in the payload
260         if new_name != project.name:
261             mongo_dict = yield self.db.projects.find_one(
262                 {"name": new_name})
263             if mongo_dict is not None:
264                 raise HTTPError(HTTP_FORBIDDEN,
265                                 "{} already exists as a project"
266                                 .format(new_name))
267
268         # new dict for changes
269         request = dict()
270         request = prepare_put_request(request,
271                                       "name",
272                                       new_name,
273                                       project.name)
274         request = prepare_put_request(request,
275                                       "description",
276                                       new_description,
277                                       project.description)
278
279         """ raise exception if there isn't a change """
280         if not request:
281             raise HTTPError(HTTP_FORBIDDEN, "Nothing to update")
282
283         """ we merge the whole document """
284         edit_request = project.format()
285         edit_request.update(request)
286
287         """ Updating the DB """
288         yield self.db.projects.update({'name': project_name}, edit_request)
289         new_project = yield self.db.projects.find_one({"_id": project._id})
290
291         self.finish_request(format_data(new_project, Project))
292
293     @asynchronous
294     @gen.coroutine
295     def delete(self, project_name):
296         """ Remove a test project"""
297
298         print "DELETE request for : {}".format(project_name)
299
300         # check for an existing project to be deleted
301         mongo_dict = yield self.db.projects.find_one(
302             {'name': project_name})
303         test_project = Project.from_dict(mongo_dict)
304         if test_project is None:
305             raise HTTPError(HTTP_NOT_FOUND,
306                             "{} could not be found as a project to be deleted"
307                             .format(project_name))
308
309         # just delete it, or maybe save it elsewhere in a future
310         yield self.db.projects.remove({'name': project_name})
311
312         self.finish_request()
313
314
315 class TestCasesHandler(GenericApiHandler):
316     """
317     TestCasesHandler Class
318     Handle the requests about the Test cases for test projects
319     HTTP Methdods :
320         - GET : Get all test cases and details about a specific one
321         - POST : Add a test project
322         - PUT : Edit test projects information (name and/or description)
323     """
324
325     def initialize(self):
326         """ Prepares the database for the entire class """
327         super(TestCasesHandler, self).initialize()
328
329     @asynchronous
330     @gen.coroutine
331     def get(self, project_name, case_name=None):
332         """
333         Get testcases(s) info
334         :param project_name:
335         :param case_name:
336         """
337
338         if case_name is None:
339             case_name = ""
340
341         get_request = dict()
342         get_request["project_name"] = project_name
343
344         if len(case_name) > 0:
345             get_request["name"] = case_name
346
347         res = []
348         cursor = self.db.test_cases.find(get_request)
349         print get_request
350         while (yield cursor.fetch_next):
351                 test_case = TestCase.test_case_from_dict(cursor.next_object())
352                 res.append(test_case.format_http())
353
354         meta = dict()
355         meta["total"] = len(res)
356         meta["success"] = True if len(res) > 0 else False
357
358         answer = dict()
359         answer["test_cases"] = res
360         answer["meta"] = meta
361
362         self.finish_request(answer)
363
364     @asynchronous
365     @gen.coroutine
366     def post(self, project_name):
367         """ Create a test case"""
368
369         print "POST Request for {}".format(project_name)
370
371         if self.json_args is None:
372             raise HTTPError(HTTP_BAD_REQUEST,
373                             "Check your request payload")
374
375         # retrieve test project
376         mongo_dict = yield self.db.projects.find_one(
377             {"name": project_name})
378         if mongo_dict is None:
379             raise HTTPError(HTTP_FORBIDDEN,
380                             "Could not find project {}"
381                             .format(project_name))
382
383         # test_project = TestProject.from_dict(self.json_args)
384
385         case = TestCase.test_case_from_dict(self.json_args)
386         case.project_name = project_name
387         case.creation_date = datetime.now()
388
389         future = self.db.test_cases.insert(case.format())
390         result = yield future
391         case._id = result
392         self.finish_request(case.format_http())
393
394     @asynchronous
395     @gen.coroutine
396     def put(self, project_name, case_name):
397         """
398         Updates the name and description of a test case
399         :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN)
400         """
401
402         print "PUT request for : {}/{}".format(project_name, case_name)
403         case_request = {'project_name': project_name, 'name': case_name}
404
405         # check if there is a case for the project in url parameters
406         mongo_dict = yield self.db.test_cases.find_one(case_request)
407         test_case = TestCase.test_case_from_dict(mongo_dict)
408         if test_case is None:
409             raise HTTPError(HTTP_NOT_FOUND,
410                             "{} could not be found as a {} case to be updated"
411                             .format(case_name, project_name))
412
413         new_name = self.json_args.get("name")
414         new_project_name = self.json_args.get("project_name")
415         new_description = self.json_args.get("description")
416
417         # check if there is not an existing test case
418         # with the name provided in the json payload
419         mongo_dict = yield self.db.test_cases.find_one(
420             {'project_name': new_project_name, 'name': new_name})
421         if mongo_dict is not None:
422             raise HTTPError(HTTP_FORBIDDEN,
423                             "{} already exists as a project"
424                             .format(new_name))
425
426         # new dict for changes
427         request = dict()
428         request = prepare_put_request(request,
429                                       "name",
430                                       new_name,
431                                       test_case.name)
432         request = prepare_put_request(request,
433                                       "project_name",
434                                       new_project_name,
435                                       test_case.project_name)
436         request = prepare_put_request(request,
437                                       "description",
438                                       new_description,
439                                       test_case.description)
440
441         # we raise an exception if there isn't a change
442         if not request:
443             raise HTTPError(HTTP_FORBIDDEN,
444                             "Nothing to update")
445
446         # we merge the whole document """
447         edit_request = test_case.format()
448         edit_request.update(request)
449
450         """ Updating the DB """
451         res = yield self.db.test_cases.update(case_request, edit_request)
452         print res
453         edit_request["_id"] = str(test_case._id)
454
455         self.finish_request({"message": "success", "content": edit_request})
456
457     @asynchronous
458     @gen.coroutine
459     def delete(self, project_name, case_name):
460         """ Remove a test case"""
461
462         print "DELETE request for : {}/{}".format(project_name, case_name)
463         case_request = {'project_name': project_name, 'name': case_name}
464
465         # check for an existing case to be deleted
466         mongo_dict = yield self.db.test_cases.find_one(case_request)
467         test_project = Project.from_dict(mongo_dict)
468         if test_project is None:
469             raise HTTPError(HTTP_NOT_FOUND,
470                             "{}/{} could not be found as a case to be deleted"
471                             .format(project_name, case_name))
472
473         # just delete it, or maybe save it elsewhere in a future
474         res = yield self.db.projects.remove(case_request)
475         print res
476
477         self.finish_request({"message": "success"})
478
479
480 class TestResultsHandler(GenericApiHandler):
481     """
482     TestResultsHandler Class
483     Handle the requests about the Test project's results
484     HTTP Methdods :
485         - GET : Get all test results and details about a specific one
486         - POST : Add a test results
487         - DELETE : Remove a test result
488     """
489
490     def initialize(self):
491         """ Prepares the database for the entire class """
492         super(TestResultsHandler, self).initialize()
493         self.name = "test_result"
494
495     @asynchronous
496     @gen.coroutine
497     def get(self, result_id=None):
498         """
499         Retrieve result(s) for a test project on a specific POD.
500         Available filters for this request are :
501          - project : project name
502          - case : case name
503          - pod : pod name
504          - version : platform version (Arno-R1, ...)
505          - installer (fuel, ...)
506          - build_tag : Jenkins build tag name
507          - period : x (x last days)
508          - scenario : the test scenario (previously version)
509          - criteria : the global criteria status passed or failed
510          - trust_indicator : evaluate the stability of the test case to avoid
511          running systematically long and stable test case
512
513
514         :param result_id: Get a result by ID
515         :raise HTTPError
516
517         GET /results/project=functest&case=vPing&version=Arno-R1 \
518         &pod=pod_name&period=15
519         => get results with optional filters
520         """
521
522         project_arg = self.get_query_argument("project", None)
523         case_arg = self.get_query_argument("case", None)
524         pod_arg = self.get_query_argument("pod", None)
525         version_arg = self.get_query_argument("version", None)
526         installer_arg = self.get_query_argument("installer", None)
527         build_tag_arg = self.get_query_argument("build_tag", None)
528         scenario_arg = self.get_query_argument("scenario", None)
529         criteria_arg = self.get_query_argument("criteria", None)
530         period_arg = self.get_query_argument("period", None)
531         trust_indicator_arg = self.get_query_argument("trust_indicator", None)
532
533         # prepare request
534         get_request = dict()
535         if result_id is None:
536             if project_arg is not None:
537                 get_request["project_name"] = project_arg
538
539             if case_arg is not None:
540                 get_request["case_name"] = case_arg
541
542             if pod_arg is not None:
543                 get_request["pod_name"] = pod_arg
544
545             if version_arg is not None:
546                 get_request["version"] = version_arg
547
548             if installer_arg is not None:
549                 get_request["installer"] = installer_arg
550
551             if build_tag_arg is not None:
552                 get_request["build_tag"] = build_tag_arg
553
554             if scenario_arg is not None:
555                 get_request["scenario"] = scenario_arg
556
557             if criteria_arg is not None:
558                 get_request["criteria_tag"] = criteria_arg
559
560             if trust_indicator_arg is not None:
561                 get_request["trust_indicator_arg"] = trust_indicator_arg
562
563             if period_arg is not None:
564                 try:
565                     period_arg = int(period_arg)
566                 except:
567                     raise HTTPError(HTTP_BAD_REQUEST)
568
569                 if period_arg > 0:
570                     period = datetime.now() - timedelta(days=period_arg)
571                     obj = {"$gte": str(period)}
572                     get_request["creation_date"] = obj
573         else:
574             get_request["_id"] = result_id
575
576         print get_request
577         res = []
578         # fetching results
579         cursor = self.db.test_results.find(get_request)
580         while (yield cursor.fetch_next):
581             test_result = TestResult.test_result_from_dict(
582                 cursor.next_object())
583             res.append(test_result.format_http())
584
585         # building meta object
586         meta = dict()
587         meta["total"] = len(res)
588
589         # final response object
590         answer = dict()
591         answer["test_results"] = res
592         answer["meta"] = meta
593         self.finish_request(answer)
594
595     @asynchronous
596     @gen.coroutine
597     def post(self):
598         """
599         Create a new test result
600         :return: status of the request
601         :raise HTTPError
602         """
603
604         # check for request payload
605         if self.json_args is None:
606             raise HTTPError(HTTP_BAD_REQUEST)
607
608         # check for missing parameters in the request payload
609         if self.json_args.get("project_name") is None:
610             raise HTTPError(HTTP_BAD_REQUEST)
611         if self.json_args.get("case_name") is None:
612             raise HTTPError(HTTP_BAD_REQUEST)
613         # check for pod_name instead of id,
614         # keeping id for current implementations
615         if self.json_args.get("pod_name") is None:
616             raise HTTPError(HTTP_BAD_REQUEST)
617
618         # TODO : replace checks with jsonschema
619         # check for project
620         mongo_dict = yield self.db.projects.find_one(
621             {"name": self.json_args.get("project_name")})
622         if mongo_dict is None:
623             raise HTTPError(HTTP_NOT_FOUND,
624                             "Could not find project [{}] "
625                             .format(self.json_args.get("project_name")))
626
627         # check for case
628         mongo_dict = yield self.db.test_cases.find_one(
629             {"name": self.json_args.get("case_name")})
630         if mongo_dict is None:
631             raise HTTPError(HTTP_NOT_FOUND,
632                             "Could not find case [{}] "
633                             .format(self.json_args.get("case_name")))
634
635         # check for pod
636         mongo_dict = yield self.db.pods.find_one(
637             {"name": self.json_args.get("pod_name")})
638         if mongo_dict is None:
639             raise HTTPError(HTTP_NOT_FOUND,
640                             "Could not find POD [{}] "
641                             .format(self.json_args.get("pod_name")))
642
643         # convert payload to object
644         test_result = TestResult.test_result_from_dict(self.json_args)
645         test_result.creation_date = datetime.now()
646
647         future = self.db.test_results.insert(test_result.format(),
648                                              check_keys=False)
649         result = yield future
650         test_result._id = result
651
652         self.finish_request(test_result.format_http())
653
654
655 class DashboardHandler(GenericApiHandler):
656     """
657     DashboardHandler Class
658     Handle the requests about the Test project's results
659     in a dahboard ready format
660     HTTP Methdods :
661         - GET : Get all test results and details about a specific one
662     """
663     def initialize(self):
664         """ Prepares the database for the entire class """
665         super(DashboardHandler, self).initialize()
666         self.name = "dashboard"
667
668     @asynchronous
669     @gen.coroutine
670     def get(self, result_id=None):
671         """
672         Retrieve dashboard ready result(s) for a test project
673         Available filters for this request are :
674          - project : project name
675          - case : case name
676          - pod : pod name
677          - version : platform version (Arno-R1, ...)
678          - installer (fuel, ...)
679          - period : x (x last days)
680
681
682         :param result_id: Get a result by ID
683         :raise HTTPError
684
685         GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
686         &pod=pod_name&period=15
687         => get results with optional filters
688         """
689
690         project_arg = self.get_query_argument("project", None)
691         case_arg = self.get_query_argument("case", None)
692         pod_arg = self.get_query_argument("pod", None)
693         version_arg = self.get_query_argument("version", None)
694         installer_arg = self.get_query_argument("installer", None)
695         period_arg = self.get_query_argument("period", None)
696
697         # prepare request
698         get_request = dict()
699
700         # /dashboard?project=<>&pod=<>...
701         if (result_id is None):
702             if project_arg is not None:
703                 get_request["project_name"] = project_arg
704
705             if case_arg is not None:
706                 get_request["case_name"] = case_arg
707
708             if pod_arg is not None:
709                 get_request["pod_name"] = pod_arg
710
711             if version_arg is not None:
712                 get_request["version"] = version_arg
713
714             if installer_arg is not None:
715                 get_request["installer"] = installer_arg
716
717             if period_arg is not None:
718                 try:
719                     period_arg = int(period_arg)
720                 except:
721                     raise HTTPError(HTTP_BAD_REQUEST)
722                 if period_arg > 0:
723                     period = datetime.now() - timedelta(days=period_arg)
724                     obj = {"$gte": str(period)}
725                     get_request["creation_date"] = obj
726         else:
727             get_request["_id"] = result_id
728
729         dashboard = []
730
731         # on /dashboard retrieve the list of projects and testcases
732         # ready for dashboard
733         if project_arg is None:
734             raise HTTPError(HTTP_NOT_FOUND,
735                             "error:Project name missing")
736         elif check_dashboard_ready_project(project_arg, "./dashboard"):
737             res = []
738
739             if case_arg is None:
740                 raise HTTPError(
741                     HTTP_NOT_FOUND,
742                     "error:Test case missing for project " + project_arg)
743
744             # special case of status for project
745             if case_arg == "status":
746                 del get_request["case_name"]
747                 # retention time to be agreed
748                 # last five days by default?
749                 # TODO move to DB
750                 period = datetime.now() - timedelta(days=5)
751                 get_request["creation_date"] = {"$gte": period}
752
753             # fetching results
754             cursor = self.db.test_results.find(get_request)
755             while (yield cursor.fetch_next):
756                 test_result = TestResult.test_result_from_dict(
757                     cursor.next_object())
758                 res.append(test_result.format_http())
759
760             if check_dashboard_ready_case(project_arg, case_arg):
761                 dashboard = get_dashboard_result(project_arg, case_arg, res)
762             else:
763                 raise HTTPError(
764                     HTTP_NOT_FOUND,
765                     "error:" + case_arg +
766                     " test case not case dashboard ready on project " +
767                     project_arg)
768
769         else:
770             dashboard.append(
771                 {"error": "Project not recognized or not dashboard ready"})
772             dashboard.append(
773                 {"Dashboard-ready-projects":
774                     get_dashboard_cases("./dashboard")})
775             raise HTTPError(
776                 HTTP_NOT_FOUND,
777                 "error: no dashboard ready data for this project")
778
779         # fetching results
780         # cursor = self.db.test_results.find(get_request)
781         # while (yield cursor.fetch_next):
782         #    test_result = TestResult.test_result_from_dict(
783         #        cursor.next_object())
784         #    res.append(test_result.format_http())
785
786         # building meta object
787         meta = dict()
788
789         # final response object
790         answer = dict()
791         answer["dashboard"] = dashboard
792         answer["meta"] = meta
793         self.finish_request(answer)