From 8f6e0965e9a6f3e2892041c06d036ab825965360 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Thu, 1 Oct 2026 15:46:13 +0300 Subject: [PATCH] =?UTF-8?q?test(resource-generator):=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=D0=B8=D0=BE=D0=BD=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20keep=5Fon=5Fdestroy=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=B4=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверяет три части фичи в сгенерированном коде: 1) поле модели KeepOnDestroy с тэгом tfsdk:keep_on_destroy; 2) атрибут схемы Optional + Default=false (поведение по умолчанию не меняется); 3) в Delete проверка флага идёт РАНЬШЕ вызова операции удаления — иначе destroy всё равно удалял бы объект в облаке. Вывод генератора перед сравнением нормализуется по пробелам: gofmt выравнивает поля структур и ключи map, из-за чего поиск подстроки «как в шаблоне» не работает. Запуск: cd TOOLS/resource-generator && go test ./internal/writers/... — ok. --- .../internal/writers/subresource_keep_test.go | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 TOOLS/resource-generator/internal/writers/subresource_keep_test.go diff --git a/TOOLS/resource-generator/internal/writers/subresource_keep_test.go b/TOOLS/resource-generator/internal/writers/subresource_keep_test.go new file mode 100644 index 0000000..e22a738 --- /dev/null +++ b/TOOLS/resource-generator/internal/writers/subresource_keep_test.go @@ -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) + } +}