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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user