test(resource-generator): регрессионный тест keep_on_destroy для подресурсов

Проверяет три части фичи в сгенерированном коде:
1) поле модели KeepOnDestroy с тэгом tfsdk:keep_on_destroy;
2) атрибут схемы Optional + Default=false (поведение по умолчанию не меняется);
3) в Delete проверка флага идёт РАНЬШЕ вызова операции удаления — иначе destroy
   всё равно удалял бы объект в облаке.

Вывод генератора перед сравнением нормализуется по пробелам: gofmt выравнивает
поля структур и ключи map, из-за чего поиск подстроки «как в шаблоне» не работает.

Запуск: cd TOOLS/resource-generator && go test ./internal/writers/... — ok.
This commit is contained in:
Repinoid
2026-10-01 15:46:13 +03:00
parent 81b85a4dea
commit 8f6e0965e9
@@ -0,0 +1,124 @@
package writers
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"resource-generator/internal/types"
)
// squashSpaces схлопывает подряд идущие пробелы/табы в один и убирает пустые
// строки. Нужно потому, что генератор прогоняет вывод через gofmt, а gofmt
// выравнивает поля структур и ключи map — сравнение «как в шаблоне» не работает.
func squashSpaces(s string) string {
s = regexp.MustCompile(`[ \t]+`).ReplaceAllString(s, " ")
return regexp.MustCompile(`\n+`).ReplaceAllString(s, "\n")
}
// TestWriteSubresource_KeepOnDestroy — регрессионный тест режима keep_on_destroy
// для подресурсов.
//
// Зачем тест: фича состоит из трёх независимых правок шаблона, и если одна из них
// отвалится при будущем рефакторинге (поле модели / атрибут схемы / ранний выход
// в Delete), фича сломается молча — генератор отработает «успешно», а destroy
// начнёт удалять объекты, которые должен был оставить. Поэтому проверяем все три
// правки и, главное, ПОРЯДОК: проверка флага обязана быть раньше вызова операции
// удаления, иначе объект всё равно уйдёт из облака.
func TestWriteSubresource_KeepOnDestroy(t *testing.T) {
dir := t.TempDir()
sr := types.GenSubresource{
ServiceName: "postgres",
ServiceID: 90,
SubName: "user",
CreateOpName: "create_user",
ModifyOpName: "modify_user",
DeleteOpName: "delete_user",
CreateParams: []types.Param{
{Code: "username", Type: "string", Required: true, Descr: "Имя пользователя"},
{Code: "password", Type: "string", Required: true, Sensitive: true, Descr: "Пароль"},
},
ModifyParams: []types.Param{
{Code: "password", Type: "string", Descr: "Пароль"},
},
DeleteParams: []types.Param{
{Code: "username", Type: "string", Required: true, Descr: "Имя пользователя"},
},
// SchemaParams — параметры, попадающие в схему (уникальный набор create+modify).
SchemaParams: []types.Param{
{Code: "username", Type: "string", Required: true, Descr: "Имя пользователя"},
{Code: "password", Type: "string", Sensitive: true, Descr: "Пароль"},
},
IdentityParams: []types.Param{
{Code: "username", Type: "string", Required: true, Descr: "Имя пользователя"},
},
UsesString: true,
HasDefaults: true,
}
if err := WriteSubresource(dir, sr); err != nil {
t.Fatalf("WriteSubresource вернул ошибку: %v", err)
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("не прочитал каталог генерации: %v", err)
}
if len(entries) != 1 {
t.Fatalf("ожидался ровно один сгенерированный файл, получено %d", len(entries))
}
raw, err := os.ReadFile(filepath.Join(dir, entries[0].Name()))
if err != nil {
t.Fatalf("не прочитал %s: %v", entries[0].Name(), err)
}
src := squashSpaces(string(raw))
// 1. Поле модели.
if !strings.Contains(src, `KeepOnDestroy types.Bool`) || !strings.Contains(src, `tfsdk:"keep_on_destroy"`) {
t.Errorf("в модели нет поля KeepOnDestroy с тэгом tfsdk:\"keep_on_destroy\"")
}
// 2. Атрибут схемы: должен быть необязательным и по умолчанию false
// (обычное удаление), иначе поведение изменится для всех существующих манифестов.
schemaIdx := strings.Index(src, `"keep_on_destroy": schema.BoolAttribute{`)
if schemaIdx < 0 {
t.Fatalf("в схеме нет атрибута keep_on_destroy")
}
schemaBlock := src[schemaIdx:]
if end := strings.Index(schemaBlock, " },"); end > 0 {
schemaBlock = schemaBlock[:end]
}
if !strings.Contains(schemaBlock, "Optional: true") {
t.Errorf("keep_on_destroy должен быть Optional")
}
if !strings.Contains(schemaBlock, "booldefault.StaticBool(false)") {
t.Errorf("keep_on_destroy по умолчанию должен быть false, блок схемы:\n%s", schemaBlock)
}
// 3. Delete: проверка флага раньше вызова операции удаления.
delIdx := strings.Index(src, ") Delete(")
if delIdx < 0 {
t.Fatalf("в сгенерированном файле нет метода Delete")
}
deleteBody := src[delIdx:]
guardIdx := strings.Index(deleteBody, "state.KeepOnDestroy.ValueBool()")
if guardIdx < 0 {
t.Fatalf("в Delete нет проверки state.KeepOnDestroy")
}
if !strings.Contains(deleteBody, "Подресурс оставлен как есть, а не удалён") {
t.Errorf("в Delete нет предупреждения о том, что подресурс оставлен в облаке")
}
callIdx := strings.Index(deleteBody, `"`+sr.DeleteOpName+`"`)
if callIdx < 0 {
t.Fatalf("в Delete нет вызова операции %q", sr.DeleteOpName)
}
if guardIdx > callIdx {
t.Errorf("проверка keep_on_destroy (%d) идёт ПОСЛЕ вызова удаления (%d): объект всё равно удалится в облаке",
guardIdx, callIdx)
}
}