feat(GODT-2788): Add JSON validator file.
This commit is contained in:
parent
ae4705ba70
commit
9b88778c43
5
Makefile
5
Makefile
|
@ -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}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue