Repository: copier-org/copier

Issue:
Bug(?) in validation of secrets
### Describe the problem

Since the new release `9.8.0` secret field with a validator leads to failing project generation.

This seems to be caused by the newly added enforcement of input validation (if I get the release notes right).

The use case is that one has:
* a secret string field;
* with an empty-string default (because a secret requires a default and because for anything non-empty a user will not be able to see the value);
* with a validator (e.g. non-empty value as in the example template, but it could be also a check on the format of the secret, e.g. that a gitlab PAT has the expected prefix)

This scenario leads to project generation failing with an trace (the same template works fine with `9.7.1`), see logs.

### Template

https://github.com/mlisovyi/tmp-copier-980-secret-with-validation

### To Reproduce

1. pip install "copier==9.8.0"
2. Run `copier copy git@github.com:mlisovyi/tmp-copier-980-secret-with-validation.git .`


### Logs

```console
Traceback (most recent call last):
  File "/path/to/venv/.venv/bin/copier", line 8, in <module>
    sys.exit(CopierApp.run())
  File "/path/to/venv/.venv/lib/python3.10/site-packages/plumbum/cli/application.py", line 640, in run
    inst, retcode = subapp.run(argv, exit=False)
  File "/path/to/venv/.venv/lib/python3.10/site-packages/plumbum/cli/application.py", line 635, in run
    retcode = inst.main(*tailargs)
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_cli.py", line 284, in main
    return _handle_exceptions(inner)
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_cli.py", line 71, in _handle_exceptions
    method()
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_cli.py", line 275, in inner
    with self._worker(
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_main.py", line 271, in __exit__
    raise value
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_cli.py", line 282, in inner
    worker.run_copy()
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_main.py", line 95, in _wrapper
    return func(*args, **kwargs)
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_main.py", line 1029, in run_copy
    self._ask()
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_main.py", line 599, in _ask
    [question.get_questionary_structure()],
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_user_data.py", line 398, in get_questionary_structure
    default = self.get_default_rendered()
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_user_data.py", line 297, in get_default_rendered
    default = self.get_default()
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_user_data.py", line 285, in get_default
    self.validate_answer(result)
  File "/path/to/venv/.venv/lib/python3.10/site-packages/copier/_user_data.py", line 455, in validate_answer
    raise ValueError(
ValueError: Validation error for question 'secret_token': Secret token cannot be empty.
```

### Expected behavior

Validator of the (required) default value for a secret has to be skipped

### Screenshots/screencasts/logs

_No response_

### Operating system

macOS

### Operating system distribution and version

Doesn't matter- also observed on Windows

### Copier version

9.8.0

### Python version

3.10 (but also observed on 3.11)

### Installation method

pip+pypi

### Additional context

_No response_


Discussion:
Thanks for reporting this bug, @mlisovyi! :bow: Sorry for the inconvenience, this was an oversight when working on #2145. 🫣

I've submitted #2254 that fixes this bug.



Patch:
diff --git a/copier/_main.py b/copier/_main.py
index 70c1e973f..380e9598b 100644
--- a/copier/_main.py
+++ b/copier/_main.py
@@ -1154,6 +1154,13 @@ def _apply_update(self) -> None:  # noqa: C901
                 src_path=self.subproject.template.url,  # type: ignore[union-attr]
                 vcs_ref=self.subproject.template.commit,  # type: ignore[union-attr]
             ) as old_worker:
+                # HACK: Remove the validator from a secret question when replaying
+                # a fresh copy using the old template because only the default value
+                # is available which may not pass validation.
+                assert old_worker.template is not self.template
+                for q in old_worker.template.questions_data.values():
+                    if q.get("secret", False):
+                        q.pop("validator", None)
                 old_worker.run_copy()
             # Run pre-migration tasks
             with Phase.use(Phase.MIGRATE):


Test patch:
diff --git a/tests/test_updatediff.py b/tests/test_updatediff.py
index 8a023a605..1d770453e 100644
--- a/tests/test_updatediff.py
+++ b/tests/test_updatediff.py
@@ -1982,3 +1982,44 @@ def test_conditional_computed_value(tmp_path_factory: pytest.TempPathFactory) ->
     assert answers["first"] is True
     assert answers["second"] is True
     assert (dst / "log.txt").read_text() == "True True"
+
+
+def test_disable_secret_validator_on_replay(
+    tmp_path_factory: pytest.TempPathFactory,
+) -> None:
+    src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
+
+    build_file_tree(
+        {
+            src / "copier.yml": (
+                """\
+                token:
+                    type: str
+                    secret: true
+                    default: ""
+                    validator: "{% if token == '' %}Must not be empty{% endif %}"
+                """
+            ),
+            src / "{{ _copier_conf.answers_file }}.jinja": (
+                "{{ _copier_answers|to_nice_yaml }}"
+            ),
+            src / ".gitignore": ".env",
+            src / ".env.jinja": "TOKEN={{ token }}",
+        }
+    )
+    with local.cwd(src):
+        git_init("v1")
+        git("tag", "v1")
+
+    run_copy(str(src), dst, data={"token": "$3cr3t"})
+    answers = load_answersfile_data(dst)
+    assert answers == {"_src_path": str(src), "_commit": "v1"}
+    assert (dst / ".env").read_text() == "TOKEN=$3cr3t"
+
+    with local.cwd(dst):
+        git_init("v1")
+
+    run_update(dst, data={"token": "$up3r-$3cr3t"}, overwrite=True)
+    answers = load_answersfile_data(dst)
+    assert answers == {"_src_path": str(src), "_commit": "v1"}
+    assert (dst / ".env").read_text() == "TOKEN=$up3r-$3cr3t"