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 \
> 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:
./utils/missing_license.sh check
@ -307,6 +307,9 @@ lint-golang:
$(info linting with GOMAXPROCS=${GOMAXPROCS})
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 -wait -cache -config utils/gobinsec_conf.yml ${EXE_TARGET} ${DEPLOY_DIR}/${TARGET_OS}/${LAUNCHER_EXE}

View File

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

View File

@ -5,27 +5,22 @@
"data_v1.0.0": {
"categories": [
{
"id": 0,
"name": "I can't receive mail",
"questions": [0,1,2,3,4]
},
{
"id": 1,
"name": "I can't send mail",
"questions": [0,1,2,3,4]
},
{
"id": 2,
"name": "Bridge is not starting",
"questions": [0,1,2,3]
},
{
"id": 3,
"name": "Bridge is slow",
"questions": [0,1,2,3]
},
{
"id": 4,
"name": "None of the above",
"questions": [0,1,2,3]
}
@ -35,7 +30,7 @@
"id": 0,
"text": "What happened?",
"tips": "Expected behavior",
"type": 1,
"type": "open",
"mandatory": true,
"maxChar": 400
},
@ -43,7 +38,7 @@
"id": 1,
"text": "What did you want or expect to happen?",
"tips": "Result",
"type": 1,
"type": "open",
"mandatory": true,
"maxChar": 400
},
@ -51,19 +46,19 @@
"id": 2,
"text": "What were the step-by-step actions you took that led to this happening?",
"tips": "Steps to reproduce",
"type": 1,
"type": "open",
"maxChar": 400
},
{
"id": 3,
"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"]
},
{
"id": 4,
"text": "Can you list the software you are running?",
"type": 3,
"type": "multichoice",
"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()