github.com/crossplane-contrib/function-cue@v0.2.2-0.20240508161918-5100fcb5a058/internal/fn/fn_test.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package fn 19 20 import ( 21 "context" 22 "encoding/json" 23 "strings" 24 "testing" 25 26 input "github.com/crossplane-contrib/function-cue/input/v1beta1" 27 "google.golang.org/protobuf/types/known/structpb" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 30 fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/require" 33 "google.golang.org/protobuf/encoding/protojson" 34 ) 35 36 func makeRequest(t *testing.T) *fnv1beta1.RunFunctionRequest { 37 var req fnv1beta1.RunFunctionRequest 38 reqJSON := ` 39 { 40 "meta": { "tag": "v1" }, 41 "observed": { 42 "composite": { 43 "resource": { 44 "apiVersion": "v1", 45 "kind": "MyKind", 46 "metadata": { 47 "annotations": { "function-cue/debug": "true" } 48 }, 49 "foo": "bar" 50 }, 51 "ready": 0 52 } 53 }, 54 "input": { 55 "foo": "bar" 56 } 57 } 58 ` 59 err := protojson.Unmarshal([]byte(reqJSON), &req) 60 require.NoError(t, err) 61 return &req 62 } 63 64 func TestEval(t *testing.T) { 65 script := ` 66 package runtime 67 #request: {...} 68 response: desired: resources: main: resource: { 69 foo: #request.observed.composite.resource.foo 70 bar: "baz" 71 } 72 ` 73 f, err := New(Options{}) 74 require.NoError(t, err) 75 req := makeRequest(t) 76 res, err := f.Eval(req, script, EvalOptions{ 77 RequestVar: "#request", 78 ResponseVar: "response", 79 Debug: DebugOptions{Enabled: true, Script: true}, 80 }) 81 require.NoError(t, err) 82 b, _ := protojson.Marshal(res) 83 blanksRemoved := strings.ReplaceAll(string(b), " ", "") 84 assert.Equal(t, `{"desired":{"resources":{"main":{"resource":{"bar":"baz","foo":"bar"}}}}}`, blanksRemoved) 85 } 86 87 func TestEvalLegacy(t *testing.T) { 88 script := ` 89 package runtime 90 _request: {...} 91 resources: main: resource: { 92 foo: _request.observed.composite.resource.foo 93 bar: "baz" 94 } 95 ` 96 f, err := New(Options{}) 97 require.NoError(t, err) 98 req := makeRequest(t) 99 res, err := f.Eval(req, script, EvalOptions{ 100 RequestVar: "_request", 101 ResponseVar: "", 102 DesiredOnlyResponse: true, 103 Debug: DebugOptions{Enabled: true, Script: true}, 104 }) 105 require.NoError(t, err) 106 b, _ := protojson.Marshal(res) 107 blanksRemoved := strings.ReplaceAll(string(b), " ", "") 108 assert.Equal(t, `{"desired":{"resources":{"main":{"resource":{"bar":"baz","foo":"bar"}}}}}`, blanksRemoved) 109 } 110 111 func TestEvalBadRuntimeCode(t *testing.T) { 112 script := ` 113 package runtime 114 request: {...} 115 response: desired: resources: main: resource: { 116 foo: request.observed.composite.resource.NO_SUCH_FIELD 117 bar: "baz" 118 } 119 ` 120 f, err := New(Options{}) 121 require.NoError(t, err) 122 req := makeRequest(t) 123 _, err = f.Eval(req, script, EvalOptions{ 124 RequestVar: "request", 125 ResponseVar: "response", 126 Debug: DebugOptions{Enabled: true, Script: true}, 127 }) 128 require.Error(t, err) 129 assert.Contains(t, err.Error(), "undefined field: NO_SUCH_FIELD") 130 } 131 132 func TestEvalBadSourceCode(t *testing.T) { 133 script := ` 134 package runtime 135 request: {...} 136 response: desired: resources: main: resource: { // no closing brace 137 ` 138 f, err := New(Options{}) 139 require.NoError(t, err) 140 req := makeRequest(t) 141 _, err = f.Eval(req, script, EvalOptions{ 142 RequestVar: "request", 143 ResponseVar: "response", 144 Debug: DebugOptions{Enabled: true, Script: true}, 145 }) 146 require.Error(t, err) 147 assert.Contains(t, err.Error(), "compile cue code: expected '}', found 'EOF'") 148 } 149 150 func TestEvalBadReturnState(t *testing.T) { 151 script := ` 152 package runtime 153 response: desired: foo: "bar" // output does not conform to the State message 154 ` 155 f, err := New(Options{}) 156 require.NoError(t, err) 157 req := makeRequest(t) 158 _, err = f.Eval(req, script, EvalOptions{ 159 RequestVar: "request", 160 ResponseVar: "response", 161 Debug: DebugOptions{Enabled: true, Script: true}, 162 }) 163 require.Error(t, err) 164 assert.Contains(t, err.Error(), "unmarshal cue output using proto json") 165 assert.Contains(t, err.Error(), `unknown field "foo"`) 166 } 167 168 func TestMergeResponse(t *testing.T) { 169 responseJSON := ` 170 { 171 "desired": { 172 "resources": { 173 "main": { 174 "resource": { "foo": "bar" }, 175 "ready": 1 176 } 177 }, 178 "composite": { 179 "resource": { "foo": "bar" }, 180 "ready": 1 181 } 182 }, 183 "context": { 184 "foo": "bar" 185 } 186 } 187 ` 188 var cueRes fnv1beta1.RunFunctionResponse 189 err := protojson.Unmarshal([]byte(responseJSON), &cueRes) 190 require.NoError(t, err) 191 var res fnv1beta1.RunFunctionResponse 192 f, err := New(Options{}) 193 require.NoError(t, err) 194 _, err = f.mergeResponse(&res, &cueRes) 195 require.NoError(t, err) 196 b, _ := protojson.Marshal(&res) 197 blanksRemoved := strings.ReplaceAll(string(b), " ", "") 198 assert.Equal(t, `{"desired":{"composite":{"resource":{"foo":"bar"},"ready":"READY_TRUE"},"resources":{"main":{"resource":{"foo":"bar"},"ready":"READY_TRUE"}}},"context":{"foo":"bar"}}`, blanksRemoved) 199 } 200 201 func TestMergeResponseWithExisting(t *testing.T) { 202 existingJSON := ` 203 { 204 "desired": { 205 "resources": { 206 "supplementary": { 207 "resource": { "foo": "bar" }, 208 "ready": 1 209 } 210 } 211 }, 212 "context": { 213 "foo": "foo2", 214 "bar": "baz" 215 } 216 } 217 ` 218 responseJSON := ` 219 { 220 "desired": { 221 "resources": { 222 "main": { 223 "resource": { "foo": "bar" }, 224 "ready": 1 225 } 226 }, 227 "composite": { 228 "resource": { "foo": "bar" }, 229 "ready": 1 230 } 231 }, 232 "context": { 233 "foo": "bar" 234 } 235 } 236 ` 237 var cueRes fnv1beta1.RunFunctionResponse 238 err := protojson.Unmarshal([]byte(responseJSON), &cueRes) 239 require.NoError(t, err) 240 var res fnv1beta1.RunFunctionResponse 241 err = protojson.Unmarshal([]byte(existingJSON), &res) 242 require.NoError(t, err) 243 f, err := New(Options{}) 244 require.NoError(t, err) 245 _, err = f.mergeResponse(&res, &cueRes) 246 require.NoError(t, err) 247 b, _ := protojson.Marshal(&res) 248 blanksRemoved := strings.ReplaceAll(string(b), " ", "") 249 assert.Equal(t, `{"desired":{"composite":{"resource":{"foo":"bar"},"ready":"READY_TRUE"},"resources":{"main":{"resource":{"foo":"bar"},"ready":"READY_TRUE"},"supplementary":{"resource":{"foo":"bar"},"ready":"READY_TRUE"}}},"context":{"bar":"baz","foo":"bar"}}`, blanksRemoved) 250 } 251 252 func TestRunFunction(t *testing.T) { 253 req := makeRequest(t) 254 script := ` 255 package runtime 256 #request: {...} 257 response: desired: resources: main: resource: { 258 foo: #request.observed.composite.resource.foo 259 bar: "baz" 260 } 261 ` 262 in := input.CueInput{ 263 TypeMeta: metav1.TypeMeta{APIVersion: "v1Aplha1", Kind: "Function"}, 264 ObjectMeta: metav1.ObjectMeta{Name: "foobar"}, 265 Script: script, 266 Debug: true, 267 } 268 b, err := json.Marshal(in) 269 require.NoError(t, err) 270 var untyped structpb.Struct 271 err = protojson.Unmarshal(b, &untyped) 272 require.NoError(t, err) 273 req.Input = &untyped 274 275 f, err := New(Options{Debug: true}) 276 require.NoError(t, err) 277 res, err := f.RunFunction(context.Background(), req) 278 require.NoError(t, err) 279 b, err = protojson.Marshal(res) 280 require.NoError(t, err) 281 blanksRemoved := strings.ReplaceAll(string(b), " ", "") 282 assert.Equal(t, `{"meta":{"tag":"v1","ttl":"60s"},"desired":{"resources":{"main":{"resource":{"bar":"baz","foo":"bar"}}}},"results":[{"severity":"SEVERITY_NORMAL","message":"cuemoduleexecutedsuccessfully"}]}`, blanksRemoved) 283 }