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