|
| 1 | +package hookinfolder_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "testing" |
| 6 | + |
| 7 | + "github.com/stretchr/testify/assert" |
| 8 | + "github.com/stretchr/testify/require" |
| 9 | + |
| 10 | + "github.com/deckhouse/module-sdk/pkg" |
| 11 | + "github.com/deckhouse/module-sdk/testing/framework" |
| 12 | + |
| 13 | + subfolder "example-module/subfolder" |
| 14 | +) |
| 15 | + |
| 16 | +// openapiDir is the path to this module's OpenAPI schemas, resolved |
| 17 | +// relative to this test file. The schemas live next to `hooks/` in |
| 18 | +// `examples/example-module/openapi/`. |
| 19 | +const openapiDir = "../../openapi" |
| 20 | + |
| 21 | +// TestOpenAPI_DefaultsSeedValuesStore demonstrates the WithOpenAPIDir |
| 22 | +// option: the framework reads the module's OpenAPI schemas and pre-seeds |
| 23 | +// the values / config-values stores with every `default:` declared by |
| 24 | +// them, exactly like addon-operator does in production. |
| 25 | +// |
| 26 | +// This is the most realistic starting point for a hook test in a module |
| 27 | +// that ships an `openapi/` directory: hooks running under the framework |
| 28 | +// observe the same defaults a real cluster would see. |
| 29 | +func TestOpenAPI_DefaultsSeedValuesStore(t *testing.T) { |
| 30 | + cfg := &pkg.HookConfig{ |
| 31 | + Metadata: pkg.HookMetadata{Name: "openapi-defaults-demo"}, |
| 32 | + OnBeforeHelm: &pkg.OrderedConfig{Order: 1}, |
| 33 | + } |
| 34 | + |
| 35 | + // A small hook that branches on the `https.mode` config value. In a |
| 36 | + // real test you would point this at one of your handlers; here we |
| 37 | + // keep it inline so the assertion is right next to the schema fields |
| 38 | + // it exercises. |
| 39 | + handler := func(_ context.Context, input *pkg.HookInput) error { |
| 40 | + mode := input.Values.Get("https.mode").String() |
| 41 | + input.Values.Set("module.runtime.https.enabled", mode != "Disabled") |
| 42 | + input.Values.Set("module.runtime.replicas", input.ConfigValues.Get("replicas").Int()) |
| 43 | + return nil |
| 44 | + } |
| 45 | + |
| 46 | + hec := framework.NewHookExecutionConfig(t, cfg, handler, |
| 47 | + framework.WithOpenAPIDir(openapiDir), |
| 48 | + ) |
| 49 | + hec.RunHook() |
| 50 | + require.NoError(t, hec.HookError()) |
| 51 | + |
| 52 | + // Defaults from openapi/config-values.yaml. |
| 53 | + assert.EqualValues(t, 1, hec.ConfigValuesGet("replicas").Int(), |
| 54 | + "replicas default should come from config-values.yaml") |
| 55 | + assert.Equal(t, "Disabled", hec.ConfigValuesGet("https.mode").String()) |
| 56 | + assert.Equal(t, "letsencrypt", |
| 57 | + hec.ConfigValuesGet("https.certManager.clusterIssuerName").String()) |
| 58 | + |
| 59 | + // Values inherit config-values via x-extend, plus get their own |
| 60 | + // schema-only fields. |
| 61 | + assert.Equal(t, "Disabled", hec.ValuesGet("https.mode").String(), |
| 62 | + "values must inherit https.mode default via x-extend") |
| 63 | + assert.True(t, hec.ValuesGet("internal.golangVersions").Exists(), |
| 64 | + "values.yaml-only defaults (internal.*) must be present") |
| 65 | + |
| 66 | + // And the hook's own writes land on top of the schema defaults. |
| 67 | + assert.False(t, hec.ValuesGet("module.runtime.https.enabled").Bool()) |
| 68 | + assert.EqualValues(t, 1, hec.ValuesGet("module.runtime.replicas").Int()) |
| 69 | +} |
| 70 | + |
| 71 | +// TestOpenAPI_UserOverridesWin pins the override semantics: anything |
| 72 | +// passed via WithInitialValues / WithInitialConfigValues is deep-merged |
| 73 | +// on top of the schema defaults, so the test author always wins. |
| 74 | +func TestOpenAPI_UserOverridesWin(t *testing.T) { |
| 75 | + cfg := &pkg.HookConfig{ |
| 76 | + Metadata: pkg.HookMetadata{Name: "openapi-user-overrides"}, |
| 77 | + OnBeforeHelm: &pkg.OrderedConfig{Order: 1}, |
| 78 | + } |
| 79 | + |
| 80 | + handler := func(_ context.Context, input *pkg.HookInput) error { |
| 81 | + input.Values.Set("module.runtime.https.enabled", |
| 82 | + input.Values.Get("https.mode").String() != "Disabled") |
| 83 | + return nil |
| 84 | + } |
| 85 | + |
| 86 | + hec := framework.NewHookExecutionConfig(t, cfg, handler, |
| 87 | + framework.WithOpenAPIDir(openapiDir), |
| 88 | + framework.WithInitialConfigValues(` |
| 89 | +replicas: 5 |
| 90 | +https: |
| 91 | + mode: CertManager |
| 92 | + certManager: |
| 93 | + clusterIssuerName: my-issuer |
| 94 | +`), |
| 95 | + framework.WithInitialValues(` |
| 96 | +https: |
| 97 | + mode: CertManager |
| 98 | +`), |
| 99 | + ) |
| 100 | + hec.RunHook() |
| 101 | + require.NoError(t, hec.HookError()) |
| 102 | + |
| 103 | + // Explicit overrides win. |
| 104 | + assert.EqualValues(t, 5, hec.ConfigValuesGet("replicas").Int()) |
| 105 | + assert.Equal(t, "CertManager", hec.ConfigValuesGet("https.mode").String()) |
| 106 | + assert.Equal(t, "my-issuer", |
| 107 | + hec.ConfigValuesGet("https.certManager.clusterIssuerName").String()) |
| 108 | + |
| 109 | + // Untouched defaults survive: customCertificate.secretName is set by |
| 110 | + // the schema and was not overridden. |
| 111 | + assert.Equal(t, "false", |
| 112 | + hec.ConfigValuesGet("https.customCertificate.secretName").String()) |
| 113 | + |
| 114 | + // And the hook's branch on https.mode picked up the override. |
| 115 | + assert.True(t, hec.ValuesGet("module.runtime.https.enabled").Bool()) |
| 116 | +} |
| 117 | + |
| 118 | +// TestOpenAPI_WithExistingHook demonstrates that WithOpenAPIDir composes |
| 119 | +// with the actual hooks shipped by this module. The values hook writes |
| 120 | +// to `some.path.to.field.*` paths that are independent of the schema, so |
| 121 | +// schema defaults and hook output coexist. |
| 122 | +func TestOpenAPI_WithExistingHook(t *testing.T) { |
| 123 | + hec := framework.NewHookExecutionConfig(t, |
| 124 | + &pkg.HookConfig{ |
| 125 | + Metadata: pkg.HookMetadata{Name: "openapi-with-existing-hook"}, |
| 126 | + OnBeforeHelm: &pkg.OrderedConfig{Order: 1}, |
| 127 | + }, |
| 128 | + subfolder.HandlerHookValues, |
| 129 | + framework.WithOpenAPIDir(openapiDir), |
| 130 | + framework.WithInitialValues(`{ |
| 131 | + "some": { |
| 132 | + "path": { |
| 133 | + "to": { |
| 134 | + "field": { |
| 135 | + "someInt": 1, |
| 136 | + "array": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | + }`), |
| 142 | + ) |
| 143 | + hec.RunHook() |
| 144 | + require.NoError(t, hec.HookError()) |
| 145 | + |
| 146 | + // Schema defaults made it through. |
| 147 | + assert.Equal(t, "Disabled", hec.ValuesGet("https.mode").String()) |
| 148 | + assert.True(t, hec.ValuesGet("internal.golangVersions").Exists()) |
| 149 | + |
| 150 | + // And the hook ran: it sets `.some.path.to.field.str` then removes |
| 151 | + // `.some.path.to.field`, so the resulting value has neither the |
| 152 | + // original nested object nor the str — the parent was removed last. |
| 153 | + assert.False(t, hec.ValuesGet("some.path.to.field").Exists(), |
| 154 | + "values hook removes the parent key as part of its handler") |
| 155 | +} |
0 commit comments