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  }