diff --git a/jipdate/cfg.py b/jipdate/cfg.py index 8a36473..86d6e65 100644 --- a/jipdate/cfg.py +++ b/jipdate/cfg.py @@ -93,6 +93,56 @@ def get_config_file(): return config_path + "/" + config_filename +def validate_yaml_structure(config): + errors = [] + server_fields = ["url", "token"] + expected_top_level = [ + "version", + "server", + "test_server", + "comments", + "header", + "use_combined_issue_header", + "separator", + "text-editor", + ] + + def check_server_section(section_name): + if section_name in config: + section = config[section_name] + if not isinstance(section, dict): + errors.append( + f"'{section_name}' must be a dictionary/mapping with nested fields" + ) + elif not set(section.keys()).intersection(server_fields): + errors.append( + f"'{section_name}' section exists but contains no expected fields - they should be indented under '{section_name}:'" + ) + + check_server_section("server") + check_server_section("test_server") + + for field in server_fields: + if field in config: + if "server" not in config and "test_server" not in config: + errors.append( + f"Found '{field}' at root level - it should be indented under 'server:' or 'test_server:'" + ) + elif "server" in config: + errors.append( + f"Found '{field}' at root level - it should be indented under 'server:'" + ) + + config_keys = set(config.keys()) if config else set() + unexpected_keys = config_keys - set(expected_top_level) - set(server_fields) + + for field_name in ["comments", "header"]: + if field_name in config and not isinstance(config[field_name], list): + errors.append(f"'{field_name}' should be a list") + + return errors + + def get_server(use_test_server=False): # Get Jira Server details. Check first if using the test server # then try user config file, then default from cfg.py @@ -105,9 +155,6 @@ def get_server(use_test_server=False): def initiate_config(): - """Reads the config file (yaml format) and returns the sets the global - instance. - """ global yml_config global config_file @@ -118,3 +165,10 @@ def initiate_config(): log.debug("Using config file: %s" % config_file) with open(config_file, "r") as yml: yml_config = yaml.load(yml, Loader=yaml.FullLoader) + + validation_errors = validate_yaml_structure(yml_config) + if validation_errors: + log.error(f"Configuration validation failed for {config_file}:") + for error in validation_errors: + log.error(f" - {error}") + sys.exit(1) diff --git a/jipdate/jiralogin.py b/jipdate/jiralogin.py index c4b7217..184370d 100644 --- a/jipdate/jiralogin.py +++ b/jipdate/jiralogin.py @@ -6,6 +6,26 @@ from jipdate import cfg from jira import JIRA from jira import JIRAError +from requests.exceptions import JSONDecodeError + + +def _get_auth_error_message(response_text, url): + """Return a user-friendly authentication error message.""" + if not response_text: + hint = "Empty response - check network connectivity" + elif any(word in response_text.lower() for word in ["login", "sign in", "captcha"]): + hint = "Authentication required - complete web login/CAPTCHA first" + elif any(word in response_text.lower() for word in ["forbidden", "access denied"]): + hint = "Access denied - check credentials and permissions" + elif any(word in response_text.lower() for word in ["maintenance", "unavailable"]): + hint = "Server maintenance - try again later" + else: + hint = "Received HTML instead of JSON - likely authentication issue" + + return ( + f"Authentication failed: {hint}\n\n" + f"To fix: Open {url} in browser, logout/login, complete any CAPTCHA, then retry." + ) def get_username_from_config(): @@ -135,15 +155,12 @@ def get_jira_instance(use_test_server): ), username, ) + except JSONDecodeError as e: + log.error(_get_auth_error_message(getattr(e, "doc", ""), url)) + sys.exit(os.EX_NOPERM) except JIRAError as e: - if e.text.find("CAPTCHA_CHALLENGE") != -1: - log.error( - "Captcha verification has been triggered by " - "JIRA - please go to JIRA using your web " - "browser, log out of JIRA, log back in " - "entering the captcha; after that is done, " - "please re-run the script" - ) + if "CAPTCHA_CHALLENGE" in e.text: + log.error(_get_auth_error_message("captcha", url)) sys.exit(os.EX_NOPERM) else: raise