From 5adda6d7921585728ee3523ffc134dd0cd03571f Mon Sep 17 00:00:00 2001
From: Joel von der Weid <joel.von-der-weid@hesge.ch>
Date: Wed, 22 May 2024 10:27:22 +0200
Subject: [PATCH] Add route to test sonar qualities

---
 ExpressAPI/assets/OpenAPI/OpenAPI.yaml    | 57 +++++++++++++++-
 ExpressAPI/src/managers/GitlabManager.ts  |  1 -
 ExpressAPI/src/managers/SonarManager.ts   | 31 ++++++++-
 ExpressAPI/src/routes/ApiRoutesManager.ts |  2 +
 ExpressAPI/src/routes/BaseRoutes.ts       | 11 ---
 ExpressAPI/src/routes/SonarRoutes.ts      | 82 +++++++++++++++++++++++
 ExpressAPI/src/shared                     |  2 +-
 7 files changed, 168 insertions(+), 18 deletions(-)
 create mode 100644 ExpressAPI/src/routes/SonarRoutes.ts

diff --git a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml
index c435bfe..7014e5c 100644
--- a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml
+++ b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml
@@ -24,10 +24,12 @@ servers:
 tags:
     -   name: General
         description: ''
+    -   name: Sonar
+        description: Routes that are used to manage SonarQube information
     -   name: Session
         description: Routes that are used to manage the user's session
     -   name: Gitlab
-        description: Routes that are used to provide Gitlab informations
+        description: Routes that are used to provide Gitlab information
     -   name: Assignment
         description: Routes that are used to manage assignments
     -   name: Exercise
@@ -48,10 +50,10 @@ paths:
                     description: OK
                 default:
                     $ref: '#/components/responses/ERROR'
-    /sonar:
+    /sonar/info:
         get:
             tags:
-                - General
+                - Sonar
             summary: Check sonar status
             description: This route can be used to check if the server supports sonar and if the integration is enabled.
             responses:
@@ -80,6 +82,55 @@ paths:
                     description: OK
                 default:
                     $ref: '#/components/responses/ERROR'
+    /sonar/testqualities:
+        post:
+            tags:
+                - Sonar
+            summary: Test existence and validity of a quality gate and quality profiles
+            description: |
+                This route should be used at assignment creation to test existence and validity of a quality gate and quality profiles before creating the assignment
+                **🔒 Security needs:** TeachingStaff or Admin roles
+            security:
+                -   Clients_Token: [ ]
+            requestBody:
+                content:
+                    multipart/form-data:
+                        schema:
+                            type: object
+                            properties:
+                                gate:
+                                    type: string
+                                profiles:
+                                    type: string
+                                    format: json
+                                    description: JSON string array of quality profiles
+                            required:
+                                - gate
+                                - profiles
+            responses:
+                '200':
+                    content:
+                        application/json:
+                            schema:
+                                allOf:
+                                    -   $ref: '#/components/schemas/DojoBackendResponse'
+                                    -   type: object
+                                        properties:
+                                            data:
+                                                type: object
+                                                properties:
+                                                    valid:
+                                                        type: boolean
+                                                    badGate:
+                                                        type: string
+                                                        description: Name of the gate if invalid, or null
+                                                    badProfiles:
+                                                        type: array
+                                                        items: string
+                                                        description: List of invalid profiles
+                    description: OK
+                default:
+                    $ref: '#/components/responses/ERROR'
     /login:
         post:
             tags:
diff --git a/ExpressAPI/src/managers/GitlabManager.ts b/ExpressAPI/src/managers/GitlabManager.ts
index e138386..3455f8b 100644
--- a/ExpressAPI/src/managers/GitlabManager.ts
+++ b/ExpressAPI/src/managers/GitlabManager.ts
@@ -148,7 +148,6 @@ class GitlabManager {
     }
 
     async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<GitlabMember> {
-        console.log(key, value);
         const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_VARIABLES_ADD).replace('{{id}}', String(repoId)), {
             key          : key,
             variable_type: 'env_var',
diff --git a/ExpressAPI/src/managers/SonarManager.ts b/ExpressAPI/src/managers/SonarManager.ts
index 9c8946e..ed08283 100644
--- a/ExpressAPI/src/managers/SonarManager.ts
+++ b/ExpressAPI/src/managers/SonarManager.ts
@@ -47,11 +47,13 @@ class SonarManager {
         })).data;
     }
 
-    private async executeGetRequest<T>(url: string) {
+    private async executeGetRequest<T>(url: string, data?: unknown) {
+
         return (await this.instance.get<T>(url, {
             headers: {
                 Authorization: `Basic ${ btoa(SharedConfig.sonar.token + ":") }`
-            }
+            },
+            params: data
         })).data;
     }
 
@@ -128,6 +130,31 @@ class SonarManager {
         const resp = await this.executeGetRequest<{ languages: { key: string, name: string }[]}>(this.getApiUrl(SonarRoute.GET_LANGUAGES))
         return resp.languages.map(l => l.key)
     }
+
+    async testQualityGate(gateName: string) {
+        try {
+            await this.executeGetRequest(this.getApiUrl(SonarRoute.TEST_GATE), { name: gateName });
+            return true;
+        } catch ( e ) {
+            return false;
+        }
+    }
+
+    async testQualityProfile(profileName: string, language: string) {
+        try {
+            const formData = new FormData();
+            formData.append('language', language);
+            formData.append('qualityProfile', profileName);
+
+            const resp = await this.executeGetRequest<{ profiles: { key: string, name: string, language: string }[] }>(
+                this.getApiUrl(SonarRoute.TEST_PROFILE), formData
+            );
+
+            return (resp.profiles.length > 0 && resp.profiles.some(p => p.name === profileName && p.language === language))
+        } catch ( e ) {
+            return false;
+        }
+    }
 }
 
 export default new SonarManager();
\ No newline at end of file
diff --git a/ExpressAPI/src/routes/ApiRoutesManager.ts b/ExpressAPI/src/routes/ApiRoutesManager.ts
index ec4eeb9..d1419c9 100644
--- a/ExpressAPI/src/routes/ApiRoutesManager.ts
+++ b/ExpressAPI/src/routes/ApiRoutesManager.ts
@@ -5,6 +5,7 @@ import SessionRoutes    from './SessionRoutes';
 import AssignmentRoutes from './AssignmentRoutes';
 import GitlabRoutes     from './GitlabRoutes';
 import ExerciseRoutes   from './ExerciseRoutes';
+import SonarRoutes      from './SonarRoutes';
 
 
 class AdminRoutesManager implements RoutesManager {
@@ -14,6 +15,7 @@ class AdminRoutesManager implements RoutesManager {
         GitlabRoutes.registerOnBackend(backend);
         AssignmentRoutes.registerOnBackend(backend);
         ExerciseRoutes.registerOnBackend(backend);
+        SonarRoutes.registerOnBackend(backend);
     }
 }
 
diff --git a/ExpressAPI/src/routes/BaseRoutes.ts b/ExpressAPI/src/routes/BaseRoutes.ts
index cdb2317..474f5bb 100644
--- a/ExpressAPI/src/routes/BaseRoutes.ts
+++ b/ExpressAPI/src/routes/BaseRoutes.ts
@@ -2,15 +2,12 @@ import { Express }     from 'express-serve-static-core';
 import express         from 'express';
 import { StatusCodes } from 'http-status-codes';
 import RoutesManager   from '../express/RoutesManager';
-import SharedSonarManager from '../shared/managers/SharedSonarManager';
-import SonarManager from '../managers/SonarManager';
 
 
 class BaseRoutes implements RoutesManager {
     registerOnBackend(backend: Express) {
         backend.get('/', this.homepage.bind(this));
         backend.get('/health_check', this.healthCheck.bind(this));
-        backend.get('/sonar', this.sonar.bind(this));
     }
 
     private async homepage(req: express.Request, res: express.Response) {
@@ -20,14 +17,6 @@ class BaseRoutes implements RoutesManager {
     private async healthCheck(req: express.Request, res: express.Response) {
         return req.session.sendResponse(res, StatusCodes.OK);
     }
-
-    private async sonar(req: express.Request, res: express.Response) {
-        const data = {
-            sonarEnabled: await SharedSonarManager.isSonarSupported(),
-            languages: await SonarManager.getLanguages()
-        };
-        return req.session.sendResponse(res, StatusCodes.OK, data);
-    }
 }
 
 
diff --git a/ExpressAPI/src/routes/SonarRoutes.ts b/ExpressAPI/src/routes/SonarRoutes.ts
new file mode 100644
index 0000000..50d7a92
--- /dev/null
+++ b/ExpressAPI/src/routes/SonarRoutes.ts
@@ -0,0 +1,82 @@
+import { Express }     from 'express-serve-static-core';
+import express         from 'express';
+import { StatusCodes } from 'http-status-codes';
+import RoutesManager   from '../express/RoutesManager';
+import SharedSonarManager from '../shared/managers/SharedSonarManager';
+import SonarManager from '../managers/SonarManager';
+import SecurityMiddleware from '../middlewares/SecurityMiddleware';
+import SecurityCheckType from '../types/SecurityCheckType';
+import * as ExpressValidator from 'express-validator';
+import DojoValidators from '../helpers/DojoValidators';
+import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware';
+
+class SonarRoutes implements RoutesManager {
+    private readonly qualitiesValidator: ExpressValidator.Schema = {
+        gate    : {
+            trim    : true,
+            notEmpty: false
+        },
+        profiles : {
+            trim           : true,
+            notEmpty       : false,
+            customSanitizer: DojoValidators.jsonSanitizer
+        }
+    };
+
+    registerOnBackend(backend: Express) {
+        backend.get('/sonar/info', this.sonar.bind(this));
+        backend.post('/sonar/testqualities', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), ParamsValidatorMiddleware.validate(this.qualitiesValidator), this.testQualities.bind(this));
+    }
+
+    private async sonar(req: express.Request, res: express.Response) {
+        const data = {
+            sonarEnabled: await SharedSonarManager.isSonarSupported(),
+            languages: await SonarManager.getLanguages()
+        };
+        return req.session.sendResponse(res, StatusCodes.OK, data);
+    }
+
+    private async testQualities(req: express.Request, res: express.Response) {
+        const params: {
+            gate: string | undefined, profiles: string[]
+        } = req.body;
+
+        console.log(params);
+
+        let gateOk = true;
+        if ((params.gate ?? "") !== "") {
+            gateOk = await SonarManager.testQualityGate(params.gate ?? "")
+        }
+
+        let profilesOk = true;
+        const badProfiles = [];
+
+        for ( const profile of params.profiles ) {
+            try {
+                const [ lang, name ] = profile.trim().split('/');
+                if ( !await SonarManager.testQualityProfile(name, lang) ) {
+                    profilesOk = false;
+                    badProfiles.push(profile);
+                }
+            } catch (e) {
+                profilesOk = false;
+                badProfiles.push(profile);
+            }
+        }
+
+        console.log(gateOk, profilesOk);
+
+        const data = {
+            valid: gateOk && profilesOk,
+            badProfiles: badProfiles,
+            badGate: (gateOk ? null : params.gate)
+        };
+
+        console.log(data);
+
+        return req.session.sendResponse(res, StatusCodes.OK, data);
+    }
+}
+
+
+export default new SonarRoutes();
diff --git a/ExpressAPI/src/shared b/ExpressAPI/src/shared
index 44b2ae3..4d1e63e 160000
--- a/ExpressAPI/src/shared
+++ b/ExpressAPI/src/shared
@@ -1 +1 @@
-Subproject commit 44b2ae365423ca96ee035d0c21bf36a27141aa79
+Subproject commit 4d1e63ebbbe7e6fec1de74d79a2919047eea5775
-- 
GitLab