1
0

feat(GODT-2788): Add JSON validator file.

This commit is contained in:
Romain Le Jeune
2023-08-01 12:47:16 +00:00
parent ae4705ba70
commit 9b88778c43
4 changed files with 232 additions and 28 deletions

View File

@ -291,7 +291,7 @@ MessageSubscriber,LabelSubscriber,AddressSubscriber,RefreshSubscriber,UserSubscr
mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity IdentityProvider,Telemetry \ mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity IdentityProvider,Telemetry \
> internal/services/useridentity/mocks/mocks.go > internal/services/useridentity/mocks/mocks.go
lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint: gofiles lint-golang lint-license lint-dependencies lint-changelog lint-bug-report
lint-license: lint-license:
./utils/missing_license.sh check ./utils/missing_license.sh check
@ -307,6 +307,9 @@ lint-golang:
$(info linting with GOMAXPROCS=${GOMAXPROCS}) $(info linting with GOMAXPROCS=${GOMAXPROCS})
golangci-lint run ./... golangci-lint run ./...
lint-bug-report:
python3 utils/validate_bug_report_file.py --file "internal/frontend/bridge-gui/bridge-gui/qml/Resources/bug_report_flow.json"
gobinsec: gobinsec-cache.yml build gobinsec: gobinsec-cache.yml build
gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE} gobinsec -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}

View File

@ -23,6 +23,9 @@ Item {
Checkbox Checkbox
} }
property string _typeOpen: "open"
property string _typeChoice: "choice"
property string _typeMutlichoice: "multichoice"
property var colorScheme property var colorScheme
property var _bottomMargin: 20 property var _bottomMargin: 20
property var _lineHeight: 1 property var _lineHeight: 1
@ -32,26 +35,26 @@ Item {
property string tips: "" property string tips: ""
property string label: "" property string label: ""
property bool mandatory: false property bool mandatory: false
property var type: QuestionItem.InputType.TextInput property var type: root._typeOpen
property var answerList: ListModel{} property var answerList: ListModel{}
property int maxChar: 150 property int maxChar: 150
property string answer:{ property string answer:{
if (type === QuestionItem.InputType.TextInput) { if (type === root._typeOpen) {
return textInput.text return textInput.text
} else if (type === QuestionItem.InputType.Radio) { } else if (type === root._typeChoice) {
return selectionRadio.text return selectionRadio.text
} else if (type === QuestionItem.InputType.Checkbox) { } else if (type === root._typeMutlichoice) {
return selectionCheckBox.text return selectionCheckBox.text
} }
return "" return ""
} }
property bool error: { property bool error: {
if (root.type === QuestionItem.InputType.TextInput) if (root.type === root._typeOpen)
return textInput.error; return textInput.error;
if (root.type === QuestionItem.InputType.Radio) if (root.type === root._typeChoice)
return selectionRadio.error; return selectionRadio.error;
if (root.type === QuestionItem.InputType.Checkbox) if (root.type === root._typeMutlichoice)
return selectionCheckBox.error; return selectionCheckBox.error;
return false return false
} }
@ -64,11 +67,11 @@ Item {
function validate() { function validate() {
if (root.type === QuestionItem.InputType.TextInput) if (root.type === root._typeOpen)
textInput.validate() textInput.validate()
else if (root.type === QuestionItem.InputType.Radio) else if (root.type === root._typeChoice)
selectionRadio.validate() selectionRadio.validate()
else if (root.type === QuestionItem.InputType.Checkbox) else if (root.type === root._typeMutlichoice)
selectionCheckBox.validate() selectionCheckBox.validate()
} }
@ -91,7 +94,7 @@ Item {
id: textInput id: textInput
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.minimumHeight: root.type === QuestionItem.InputType.TextInput ? heightForLinesVisible(2) : 0 Layout.minimumHeight: root.type === root._typeOpen ? heightForLinesVisible(2) : 0
colorScheme: root.colorScheme colorScheme: root.colorScheme
property int _maxLength: root.maxChar property int _maxLength: root.maxChar
@ -102,7 +105,7 @@ Item {
placeholderText: mandatory ? qsTr("%1... (min. %2 characters)").arg(root.text).arg(_minLength) : "" placeholderText: mandatory ? qsTr("%1... (min. %2 characters)").arg(root.text).arg(_minLength) : ""
function setDefaultValue(defaultValue) { function setDefaultValue(defaultValue) {
textInput.text = root.type === QuestionItem.InputType.TextInput ? defaultValue : "" textInput.text = root.type === root._typeOpen ? defaultValue : ""
} }
validator: function (text) { validator: function (text) {
@ -121,7 +124,7 @@ Item {
} }
} }
visible: root.type === QuestionItem.InputType.TextInput visible: root.type === root._typeOpen
} }
ButtonGroup { ButtonGroup {
@ -133,7 +136,7 @@ Item {
property bool error: root.mandatory property bool error: root.mandatory
function setDefaultValue(defaultValue) { function setDefaultValue(defaultValue) {
const values = root.type === QuestionItem.InputType.Radio ? defaultValue : []; const values = root.type === root._typeChoice ? defaultValue : [];
for (var i = 0; i < buttons.length; ++i) { for (var i = 0; i < buttons.length; ++i) {
buttons[i].checked = values.includes(buttons[i].text); buttons[i].checked = values.includes(buttons[i].text);
} }
@ -157,7 +160,7 @@ Item {
ButtonGroup.group: selectionRadio ButtonGroup.group: selectionRadio
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: modelData text: modelData
visible: root.type === QuestionItem.InputType.Radio visible: root.type === root._typeChoice
} }
} }
ButtonGroup { ButtonGroup {
@ -176,7 +179,7 @@ Item {
property bool error: root.mandatory property bool error: root.mandatory
function setDefaultValue(defaultValue) { function setDefaultValue(defaultValue) {
const values = root.type === QuestionItem.InputType.Checkbox ? defaultValue.split(delimitor) : []; const values = root.type === root._typeMutlichoice ? defaultValue.split(delimitor) : [];
for (var i = 0; i < buttons.length; ++i) { for (var i = 0; i < buttons.length; ++i) {
buttons[i].checked = values.includes(buttons[i].text); buttons[i].checked = values.includes(buttons[i].text);
} }
@ -201,7 +204,7 @@ Item {
ButtonGroup.group: selectionCheckBox ButtonGroup.group: selectionCheckBox
colorScheme: root.colorScheme colorScheme: root.colorScheme
text: modelData text: modelData
visible: root.type === QuestionItem.InputType.Checkbox visible: root.type === root._typeMutlichoice
} }
} }
} }

View File

@ -5,27 +5,22 @@
"data_v1.0.0": { "data_v1.0.0": {
"categories": [ "categories": [
{ {
"id": 0,
"name": "I can't receive mail", "name": "I can't receive mail",
"questions": [0,1,2,3,4] "questions": [0,1,2,3,4]
}, },
{ {
"id": 1,
"name": "I can't send mail", "name": "I can't send mail",
"questions": [0,1,2,3,4] "questions": [0,1,2,3,4]
}, },
{ {
"id": 2,
"name": "Bridge is not starting", "name": "Bridge is not starting",
"questions": [0,1,2,3] "questions": [0,1,2,3]
}, },
{ {
"id": 3,
"name": "Bridge is slow", "name": "Bridge is slow",
"questions": [0,1,2,3] "questions": [0,1,2,3]
}, },
{ {
"id": 4,
"name": "None of the above", "name": "None of the above",
"questions": [0,1,2,3] "questions": [0,1,2,3]
} }
@ -35,7 +30,7 @@
"id": 0, "id": 0,
"text": "What happened?", "text": "What happened?",
"tips": "Expected behavior", "tips": "Expected behavior",
"type": 1, "type": "open",
"mandatory": true, "mandatory": true,
"maxChar": 400 "maxChar": 400
}, },
@ -43,7 +38,7 @@
"id": 1, "id": 1,
"text": "What did you want or expect to happen?", "text": "What did you want or expect to happen?",
"tips": "Result", "tips": "Result",
"type": 1, "type": "open",
"mandatory": true, "mandatory": true,
"maxChar": 400 "maxChar": 400
}, },
@ -51,19 +46,19 @@
"id": 2, "id": 2,
"text": "What were the step-by-step actions you took that led to this happening?", "text": "What were the step-by-step actions you took that led to this happening?",
"tips": "Steps to reproduce", "tips": "Steps to reproduce",
"type": 1, "type": "open",
"maxChar": 400 "maxChar": 400
}, },
{ {
"id": 3, "id": 3,
"text": "Can you reproduce this issue? (If you repeat the actions, the same thing happens.)", "text": "Can you reproduce this issue? (If you repeat the actions, the same thing happens.)",
"type": 2, "type": "choice",
"answerList": ["Yes", "No", "I don't know"] "answerList": ["Yes", "No", "I don't know"]
}, },
{ {
"id": 4, "id": 4,
"text": "Can you list the software you are running?", "text": "Can you list the software you are running?",
"type": 3, "type": "multichoice",
"answerList": ["VPN", "Antivirus", "Firewall", "Cache cleaner"] "answerList": ["VPN", "Antivirus", "Firewall", "Cache cleaner"]
} }
] ]

203
utils/validate_bug_report_file.py Executable file
View File

@ -0,0 +1,203 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2023 Proton AG
#
# This file is part of Proton Mail Bridge.
#
# Proton Mail Bridge is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Proton Mail Bridge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
import argparse
import json
import re
class BugReportJson:
def __init__(self, filepath):
self.filepath = filepath
self.json = None
self.metadata = None
self.version = None
self.data = None
self.categories = None
self.questions = None
self.questionsID = []
self.error = ""
def validate(self):
with open(self.filepath) as infile:
self.json = json.load(infile)
if self.json is None:
return False, ("JSON cannot be load from %s." % self.filepath)
for object in self.json:
if not (object == "metadata" or re.match(r"data_v[0-9]+\.[0-9]+\.[0-9]+", object)) :
self.error = ("Unexpected object name %s." % object)
return False
if not self.parse_metadata():
return False
if not self.parse_data():
return False
if not self.parse_questions():
return False
if not self.parse_categories():
return False
return True
def parse_metadata(self):
if "metadata" not in self.json:
self.error = "No metadata object."
return False
if not isinstance(self.json["metadata"], dict):
self.error = "metadata should be a dictionary."
return False
self.metadata = self.json["metadata"]
if "version" not in self.metadata:
self.error = "No version in metadata object."
return False
self.version = self.metadata["version"]
if not re.match(r"[0-9]+\.[0-9]+\.[0-9]+", self.version):
self.error = ("Version (%s) doesn't match pattern." % self.version)
return False
return True
def parse_data(self):
data_version = ("data_v%s" % self.version)
if data_version not in self.json:
self.error = ("No data object matching version %s." % self.version)
return False
if not isinstance(self.json[data_version], dict):
self.error = ("%s should be a dictionary." %data_version)
return False
self.data = self.json[data_version]
if "categories" not in self.data:
self.error = "No categories object in data."
return False
self.categories = self.data["categories"]
if not isinstance(self.categories, list):
self.error = "categories should be an array."
return False
if "questions" not in self.data:
self.error = "No questions object in data."
return False
self.questions = self.data["questions"]
if not isinstance(self.questions, list):
self.error = "questions should be an array."
return False
return True
def parse_questions(self):
for question in self.questions:
if not isinstance(question, dict):
self.error = ("Question should be a dictionary.")
return False
for option in question:
if option not in ["id", "text", "tips", "type", "mandatory", "maxChar", "answerList"]:
self.error = ("Unexpected option '%s' in question." % option)
return False
# check mandatory field
if "id" not in question:
self.error = ("Missing id in question %s." % question)
return False
if question["id"] in self.questionsID:
self.error = ("Question id should be unique (%d)." % question["id"])
return False
self.questionsID.append(question["id"])
if "text" not in question:
self.error = ("Missing text in question %s." % question)
return False
if "type" not in question:
self.error = ("Missing type in question %s." % question)
return False
# check type restriction
if question["type"] == "open":
if "maxChar" in question:
if question["maxChar"] > 1000:
self.error = ("MaxChar is too damn high in question %s." % question)
return False
if "answerList" in question:
self.error = ("AnswerList should not be present in open question %s." % question)
return False
elif question["type"] == "choice" or question["type"] == "multichoice":
if "answerList" not in question:
self.error = ("Missing answerList in question %s." % question)
return False
if not isinstance(question["answerList"], list):
self.error = ("AnswerList should be an array in question %s." % question)
return False
if "maxChar" in question:
self.error = ("maxChar should not be present in choice/multichoice question %s." % question)
return False
else:
self.error = ("Wrong type in question %s." % question)
return False
return True
def parse_categories(self):
for category in self.categories:
if not isinstance(category, dict):
self.error = ("category should be a dictionary.")
return False
for option in category:
if option not in ["name", "questions"]:
self.error = ("Unexpected option '%s' in category." % option)
return False
if "name" not in category:
self.error = ("Missing name in category %s." % category)
return False
if "questions" not in category:
self.error = ("Missing questions in category %s." % category)
return False
unique_list = []
for question in category["questions"]:
if question not in self.questionsID:
self.error = ("Questions referring to non-existing question in category %s." % category)
return False
if question in unique_list:
self.error = ("Questions contains duplicate in category %s." % category)
return False
unique_list.append(question)
return True
def parse_args():
parser = argparse.ArgumentParser(description='Validate Bug Report File.')
parser.add_argument('--file', required=True, help='JSON file to validate.')
return parser.parse_args()
def main():
args = parse_args()
report = BugReportJson(args.file)
if not report.validate():
print("Validation FAILED for %s. Error: %s" %(report.filepath, report.error))
exit(1)
print("Validation SUCCEED for %s." % report.filepath)
exit(0)
if __name__ == "__main__":
main()