github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/togglebutton/reconciler_test.go (about) 1 package togglebutton 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/stretchr/testify/require" 8 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 "k8s.io/apimachinery/pkg/types" 10 11 "github.com/tilt-dev/tilt/internal/controllers/fake" 12 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 13 ) 14 15 var tbName = types.NamespacedName{Name: "test-toggle"} 16 17 func TestReconciler_CreatesOffUIButton(t *testing.T) { 18 f := newFixture(t) 19 20 f.setupTest() 21 22 uib := f.uiButton() 23 require.Equal(t, "enable", uib.Spec.Text) 24 25 f.requireSteadyState() 26 } 27 28 func TestReconciler_CreatesOnUIButton(t *testing.T) { 29 f := newFixture(t) 30 31 f.setupTest() 32 33 cm := f.configMap() 34 cm.Data["enabled"] = "true" 35 require.NoError(t, f.Client.Update(f.ctx, &cm)) 36 37 f.MustReconcile(tbName) 38 39 uib := f.uiButton() 40 require.Equal(t, "disable", uib.Spec.Text) 41 42 f.requireSteadyState() 43 } 44 45 func TestReconciler_DeletesUIButton(t *testing.T) { 46 f := newFixture(t) 47 48 f.setupTest() 49 50 uib := f.uiButton() 51 require.NotNil(t, uib) 52 53 tb := f.toggleButton() 54 found, _ := f.Delete(&tb) 55 require.True(t, found) 56 57 found = f.Get(f.KeyForObject(&uib), &uib) 58 require.False(t, found) 59 } 60 61 func TestReconciler_HandlesClick(t *testing.T) { 62 f := newFixture(t) 63 64 f.setupTest() 65 66 // simulate a click on the button 67 uib := f.uiButton() 68 require.NotNil(t, uib) 69 uib.Status.LastClickedAt = metav1.NowMicro() 70 uib.Status.Inputs = append(uib.Status.Inputs, v1alpha1.UIInputStatus{ 71 Name: uib.Spec.Inputs[0].Name, 72 Hidden: &v1alpha1.UIHiddenInputStatus{Value: uib.Spec.Inputs[0].Hidden.Value}, 73 }) 74 err := f.Client.Status().Update(f.ctx, &uib) 75 require.NoError(t, err) 76 77 f.MustReconcile(tbName) 78 79 // now we should see: 80 // 1. The ConfigMap gets updated 81 cm := f.configMap() 82 require.Equal(t, "true", cm.Data["enabled"]) 83 84 // 2. The UIButton reflects the TB's OnStateSpec 85 uib = f.uiButton() 86 require.Equal(t, "disable", uib.Spec.Text) 87 88 f.requireSteadyState() 89 } 90 91 func TestReconciler_HandlesConfigMapUpdate(t *testing.T) { 92 f := newFixture(t) 93 94 f.setupTest() 95 96 cm := f.configMap() 97 cm.Data["enabled"] = "true" 98 err := f.Client.Update(f.ctx, &cm) 99 require.NoError(t, err) 100 101 f.MustReconcile(tbName) 102 103 // changing the configmap directly should cause the uibutton to update 104 uib := f.uiButton() 105 require.Equal(t, "disable", uib.Spec.Text) 106 107 f.requireSteadyState() 108 } 109 110 func TestReconciler_uiButtonClickedNoInput(t *testing.T) { 111 f := newFixture(t) 112 113 f.setupTest() 114 115 // simulate a click on the button, but without the expected input 116 uib := f.uiButton() 117 require.NotNil(t, uib) 118 uib.Status.LastClickedAt = metav1.NowMicro() 119 err := f.Client.Status().Update(f.ctx, &uib) 120 require.NoError(t, err) 121 122 f.MustReconcile(tbName) 123 124 tb := f.toggleButton() 125 require.Contains(t, tb.Status.Error, "does not have an input named \"action\"") 126 127 f.requireSteadyState() 128 } 129 130 func TestReconciler_uiButtonClickedInputWrongType(t *testing.T) { 131 f := newFixture(t) 132 133 f.setupTest() 134 135 // simulate a click on the button, but with the wrong input type 136 uib := f.uiButton() 137 require.NotNil(t, uib) 138 uib.Status.LastClickedAt = metav1.NowMicro() 139 uib.Status.Inputs = []v1alpha1.UIInputStatus{ 140 { 141 Name: actionUIInputName, 142 Text: &v1alpha1.UITextInputStatus{Value: turnOnInputValue}, 143 }, 144 } 145 err := f.Client.Status().Update(f.ctx, &uib) 146 require.NoError(t, err) 147 148 f.MustReconcile(tbName) 149 150 tb := f.toggleButton() 151 require.Contains(t, tb.Status.Error, "input \"action\" was not of type 'Hidden'") 152 153 f.requireSteadyState() 154 } 155 156 func TestReconciler_uiButtonClickedInputWrongValue(t *testing.T) { 157 f := newFixture(t) 158 159 f.setupTest() 160 161 // simulate a click on the button, but with an unknown value 162 uib := f.uiButton() 163 require.NotNil(t, uib) 164 uib.Status.LastClickedAt = metav1.NowMicro() 165 uib.Status.Inputs = []v1alpha1.UIInputStatus{ 166 { 167 Name: actionUIInputName, 168 Hidden: &v1alpha1.UIHiddenInputStatus{Value: "fdasfsa"}, 169 }, 170 } 171 err := f.Client.Status().Update(f.ctx, &uib) 172 require.NoError(t, err) 173 174 f.MustReconcile(tbName) 175 176 tb := f.toggleButton() 177 require.Contains(t, tb.Status.Error, "unexpected value \"fdasfsa\"") 178 179 f.requireSteadyState() 180 } 181 182 func TestReconciler_noConfigMap(t *testing.T) { 183 f := newFixture(t) 184 185 f.createToggleButton() 186 187 tb := f.toggleButton() 188 require.Equal(t, "no such ConfigMap \"toggle-cm\"", tb.Status.Error) 189 190 f.requireSteadyState() 191 } 192 193 func TestReconciler_doesntSpecifyConfigMap(t *testing.T) { 194 f := newFixture(t) 195 196 f.createToggleButton() 197 tb := f.toggleButton() 198 tb.Spec.StateSource.ConfigMap = nil 199 f.Update(&tb) 200 201 // simulate a click on the button, but with an unknown value 202 uib := f.uiButton() 203 require.NotNil(t, uib) 204 uib.Status.LastClickedAt = metav1.NowMicro() 205 uib.Status.Inputs = []v1alpha1.UIInputStatus{ 206 { 207 Name: actionUIInputName, 208 Hidden: &v1alpha1.UIHiddenInputStatus{Value: "foo"}, 209 }, 210 } 211 err := f.Client.Status().Update(f.ctx, &uib) 212 require.NoError(t, err) 213 214 f.MustReconcile(tbName) 215 216 tb = f.toggleButton() 217 require.Contains(t, tb.Status.Error, "Spec.StateSource.ConfigMap is nil") 218 219 f.requireSteadyState() 220 } 221 222 func TestReconciler_ConfigMapUnexpectedValue(t *testing.T) { 223 f := newFixture(t) 224 f.setupTest() 225 226 cm := f.configMap() 227 tb := f.toggleButton() 228 cm.Data[tb.Spec.StateSource.ConfigMap.Key] = "asdf" 229 err := f.Client.Update(f.ctx, &cm) 230 require.NoError(t, err) 231 232 f.MustReconcile(tbName) 233 234 tb = f.toggleButton() 235 require.Contains(t, tb.Status.Error, "unknown value \"asdf\"") 236 237 f.requireSteadyState() 238 } 239 240 func TestReconciler_clearError(t *testing.T) { 241 f := newFixture(t) 242 f.setupTest() 243 244 cm := f.configMap() 245 tb := f.toggleButton() 246 cm.Data[tb.Spec.StateSource.ConfigMap.Key] = "asdf" 247 err := f.Client.Status().Update(f.ctx, &cm) 248 require.NoError(t, err) 249 f.MustReconcile(tbName) 250 251 cm.Data[tb.Spec.StateSource.ConfigMap.Key] = tb.Spec.StateSource.ConfigMap.OnValue 252 err = f.Client.Status().Update(f.ctx, &cm) 253 require.NoError(t, err) 254 f.MustReconcile(tbName) 255 256 tb = f.toggleButton() 257 require.Equal(t, "", tb.Status.Error) 258 259 f.requireSteadyState() 260 } 261 262 type fixture struct { 263 *fake.ControllerFixture 264 t *testing.T 265 ctx context.Context 266 } 267 268 func newFixture(t *testing.T) *fixture { 269 cfb := fake.NewControllerFixtureBuilder(t) 270 r := NewReconciler(cfb.Client, cfb.Client.Scheme()) 271 return &fixture{ 272 ControllerFixture: cfb.Build(r), 273 t: t, 274 ctx: context.Background(), 275 } 276 } 277 278 func (f *fixture) createConfigMap() { 279 err := f.Client.Create(f.ctx, &v1alpha1.ConfigMap{ 280 ObjectMeta: metav1.ObjectMeta{ 281 Name: "toggle-cm", 282 }, 283 Data: map[string]string{ 284 "enabled": "false", 285 }, 286 }) 287 require.NoError(f.t, err) 288 } 289 290 func (f *fixture) createToggleButton() { 291 err := f.Client.Create(f.ctx, &v1alpha1.ToggleButton{ 292 ObjectMeta: metav1.ObjectMeta{ 293 Name: tbName.Name, 294 Namespace: tbName.Namespace, 295 }, 296 Spec: v1alpha1.ToggleButtonSpec{ 297 On: v1alpha1.ToggleButtonStateSpec{ 298 Text: "disable", 299 IconName: "stop", 300 }, 301 Off: v1alpha1.ToggleButtonStateSpec{ 302 Text: "enable", 303 IconName: "play_arrow", 304 }, 305 DefaultOn: false, 306 StateSource: v1alpha1.StateSource{ 307 ConfigMap: &v1alpha1.ConfigMapStateSource{ 308 Name: "toggle-cm", 309 Key: "enabled", 310 OnValue: "true", 311 OffValue: "false", 312 }, 313 }, 314 }, 315 }) 316 require.NoError(f.t, err) 317 f.MustReconcile(tbName) 318 } 319 320 func (f *fixture) setupTest() { 321 f.createConfigMap() 322 f.createToggleButton() 323 } 324 325 func (f *fixture) toggleButton() v1alpha1.ToggleButton { 326 var tb v1alpha1.ToggleButton 327 f.MustGet(tbName, &tb) 328 return tb 329 } 330 331 func (f *fixture) uiButton() v1alpha1.UIButton { 332 var uib v1alpha1.UIButton 333 f.MustGet(types.NamespacedName{Name: "toggle-test-toggle"}, &uib) 334 return uib 335 } 336 337 func (f *fixture) configMap() v1alpha1.ConfigMap { 338 var cm v1alpha1.ConfigMap 339 f.MustGet(types.NamespacedName{Name: "toggle-cm"}, &cm) 340 return cm 341 } 342 343 // ensures a reconcile doesn't cause any object changes and thus risk infinitely reconciling 344 func (f *fixture) requireSteadyState() { 345 tb := f.toggleButton() 346 f.MustReconcile(tbName) 347 tb2 := f.toggleButton() 348 require.Equal(f.t, tb.ObjectMeta.ResourceVersion, tb2.ObjectMeta.ResourceVersion) 349 }