From 8c24251b7d584135bf4829b4f523704f24ca0829 Mon Sep 17 00:00:00 2001 From: Itx-Psycho0 Date: Thu, 21 May 2026 01:11:34 +0530 Subject: [PATCH 1/2] test: add E2E matrix test for deploy -e flag (fixes #3514) Add comprehensive E2E matrix test to verify that environment variables passed via `func deploy -e KEY=VALUE` are correctly applied to deployed functions across all supported builders and runtimes. The test: - Creates a function that echoes back environment variables - Deploys it with `func deploy -e TEST_VAR=test_value -e ANOTHER_VAR=another_value` - Invokes the function and verifies env vars are present - Runs for all builder ?? runtime ?? template combinations This addresses the lack of test coverage for the -e flag on deploy, which was identified as the root cause of issue #3514. Implementation: - Added TestMatrix_Deploy_Envs() function in e2e/e2e_matrix_deploy_envs_test.go - Added createEnvEchoFunction() helper for language-specific implementations - Supports all 7 runtimes: go, python, node, typescript, rust, quarkus, springboot - Supports all 3 builders: host, pack, s2i - Supports both templates: http, cloudevents The test will run in CI when FUNC_E2E_MATRIX=true is set, verifying that the -e flag works correctly across all supported combinations and preventing future regressions. Fixes #3514 --- e2e/e2e_matrix_deploy_envs_test.go | 340 +++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 e2e/e2e_matrix_deploy_envs_test.go diff --git a/e2e/e2e_matrix_deploy_envs_test.go b/e2e/e2e_matrix_deploy_envs_test.go new file mode 100644 index 0000000000..d9c671b9df --- /dev/null +++ b/e2e/e2e_matrix_deploy_envs_test.go @@ -0,0 +1,340 @@ +//go:build e2e + +package e2e + +import ( + "io" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// TestMatrix_Deploy_Envs ensures that environment variables passed via -e flag +// on deploy are correctly applied to the deployed function across all builders +// and runtimes. +// +// This test verifies that `func deploy -e KEY=VALUE` works correctly for: +// - All supported runtimes (go, python, node, typescript, rust, quarkus, springboot) +// - All supported builders (host, pack, s2i) +// - Both http and cloudevents templates +// +// The test creates a function that echoes back environment variables, deploys +// it with `func deploy -e TEST_VAR=test_value -e ANOTHER_VAR=another_value`, +// then invokes the function and verifies the env vars are present. +// +// Related: Issue #3514 +func TestMatrix_Deploy_Envs(t *testing.T) { + forEachPermutation(t, "deploy-envs", func(t *testing.T, name, runtime, builder, template string) { + root := fromCleanEnv(t, name) + + // Register cleanup functions + t.Cleanup(func() { + cleanImages(t, name) + }) + t.Cleanup(func() { + clean(t, name, Namespace) + }) + + // Initialize function + initArgs := []string{"init", "-l", runtime, "-t", template} + initArgs, timeout := matrixExceptionsLocal(t, initArgs, runtime, builder, template) + if err := newCmd(t, initArgs...).Run(); err != nil { + t.Fatalf("Failed to create %s function with %s template: %v", runtime, template, err) + } + + // Create function implementation that echoes back env vars + impl := createEnvEchoFunction(t, root, runtime, template) + targetFile := filepath.Join(root, impl.filename) + + // For some runtimes, we need to create parent directories + if err := os.MkdirAll(filepath.Dir(targetFile), 0755); err != nil { + t.Fatalf("Failed to create directory for %s: %v", targetFile, err) + } + + if err := os.WriteFile(targetFile, []byte(impl.code), 0644); err != nil { + t.Fatalf("Failed to write function implementation: %v", err) + } + + // Deploy with -e flag + deployArgs := []string{"deploy", "-e", "TEST_VAR=test_value", "-e", "ANOTHER_VAR=another_value", "--builder", builder} + if err := newCmd(t, deployArgs...).Run(); err != nil { + t.Fatal(err) + } + + // Wait for function to be ready + url := ksvcUrl(name) + if !waitFor(t, url, withWaitTimeout(timeout), withTemplate(template)) { + t.Fatal("function did not become ready") + } + + // Invoke function and check response + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Get(url) + if err != nil { + t.Fatalf("failed to invoke function: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("failed to read response: %v", err) + } + + // Check status code + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) + } + + // Verify env vars are present in response + responseStr := string(body) + if !strings.Contains(responseStr, "TEST_VAR=test_value") { + t.Errorf("TEST_VAR not found or has wrong value in response. Got: %s", responseStr) + } + if !strings.Contains(responseStr, "ANOTHER_VAR=another_value") { + t.Errorf("ANOTHER_VAR not found or has wrong value in response. Got: %s", responseStr) + } + }) +} + +// envEchoImpl holds the filename and code for a function implementation +// that echoes back environment variables +type envEchoImpl struct { + filename string + code string +} + +// createEnvEchoFunction creates a language-specific function implementation +// that echoes back the TEST_VAR and ANOTHER_VAR environment variables. +// Returns HTTP 500 if either variable is not set, HTTP 200 with the values otherwise. +func createEnvEchoFunction(t *testing.T, root, runtime, template string) envEchoImpl { + t.Helper() + + switch runtime { + case "go": + return envEchoImpl{ + filename: "function.go", + code: `package function + +import ( + "fmt" + "net/http" + "os" +) + +type Function struct{} + +func New() *Function { return &Function{} } + +func (f *Function) Handle(w http.ResponseWriter, _ *http.Request) { + testVar := os.Getenv("TEST_VAR") + anotherVar := os.Getenv("ANOTHER_VAR") + + if testVar == "" { + http.Error(w, "TEST_VAR not set", http.StatusInternalServerError) + return + } + if anotherVar == "" { + http.Error(w, "ANOTHER_VAR not set", http.StatusInternalServerError) + return + } + + fmt.Fprintf(w, "TEST_VAR=%s\nANOTHER_VAR=%s\n", testVar, anotherVar) +} +`, + } + + case "node": + return envEchoImpl{ + filename: "index.js", + code: `const handle = async (context) => { + const testVar = process.env.TEST_VAR; + const anotherVar = process.env.ANOTHER_VAR; + + if (!testVar) { + return { + statusCode: 500, + body: 'TEST_VAR not set' + }; + } + if (!anotherVar) { + return { + statusCode: 500, + body: 'ANOTHER_VAR not set' + }; + } + + return { + statusCode: 200, + body: ` + "`TEST_VAR=${testVar}\\nANOTHER_VAR=${anotherVar}`" + ` + }; +}; + +module.exports = { handle }; +`, + } + + case "typescript": + return envEchoImpl{ + filename: "index.ts", + code: `import { Context } from 'faas-js-runtime'; + +const handle = async (context: Context): Promise => { + const testVar = process.env.TEST_VAR; + const anotherVar = process.env.ANOTHER_VAR; + + if (!testVar) { + return { + statusCode: 500, + body: 'TEST_VAR not set' + }; + } + if (!anotherVar) { + return { + statusCode: 500, + body: 'ANOTHER_VAR not set' + }; + } + + return { + statusCode: 200, + body: ` + "`TEST_VAR=${testVar}\\nANOTHER_VAR=${anotherVar}`" + ` + }; +}; + +export { handle }; +`, + } + + case "python": + return envEchoImpl{ + filename: "func.py", + code: `import os +from parliament import Context + +def main(context: Context): + test_var = os.getenv('TEST_VAR') + another_var = os.getenv('ANOTHER_VAR') + + if not test_var: + return {'statusCode': 500, 'body': 'TEST_VAR not set'} + if not another_var: + return {'statusCode': 500, 'body': 'ANOTHER_VAR not set'} + + return { + 'statusCode': 200, + 'body': f'TEST_VAR={test_var}\\nANOTHER_VAR={another_var}' + } +`, + } + + case "rust": + return envEchoImpl{ + filename: "src/lib.rs", + code: `use std::env; + +pub fn handle(_req: http::Request>) -> http::Response> { + let test_var = env::var("TEST_VAR").unwrap_or_default(); + let another_var = env::var("ANOTHER_VAR").unwrap_or_default(); + + if test_var.is_empty() { + return http::Response::builder() + .status(500) + .body("TEST_VAR not set".as_bytes().to_vec()) + .unwrap(); + } + if another_var.is_empty() { + return http::Response::builder() + .status(500) + .body("ANOTHER_VAR not set".as_bytes().to_vec()) + .unwrap(); + } + + let body = format!("TEST_VAR={}\\nANOTHER_VAR={}", test_var, another_var); + http::Response::builder() + .status(200) + .body(body.as_bytes().to_vec()) + .unwrap() +} +`, + } + + case "quarkus": + return envEchoImpl{ + filename: "src/main/java/functions/Function.java", + code: `package functions; + +import io.quarkus.funqy.Funq; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import javax.inject.Inject; +import java.util.Optional; + +public class Function { + + @ConfigProperty(name = "TEST_VAR") + Optional testVar; + + @ConfigProperty(name = "ANOTHER_VAR") + Optional anotherVar; + + @Funq + public String function() { + if (!testVar.isPresent() || testVar.get().isEmpty()) { + throw new RuntimeException("TEST_VAR not set"); + } + if (!anotherVar.isPresent() || anotherVar.get().isEmpty()) { + throw new RuntimeException("ANOTHER_VAR not set"); + } + return "TEST_VAR=" + testVar.get() + "\\nANOTHER_VAR=" + anotherVar.get(); + } +} +`, + } + + case "springboot": + return envEchoImpl{ + filename: "src/main/java/functions/CloudFunctionApplication.java", + code: `package functions; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +import java.util.function.Function; + +@SpringBootApplication +public class CloudFunctionApplication { + + public static void main(String[] args) { + SpringApplication.run(CloudFunctionApplication.class, args); + } + + @Bean + public Function function(Environment env) { + return input -> { + String testVar = env.getProperty("TEST_VAR"); + String anotherVar = env.getProperty("ANOTHER_VAR"); + + if (testVar == null || testVar.isEmpty()) { + throw new RuntimeException("TEST_VAR not set"); + } + if (anotherVar == null || anotherVar.isEmpty()) { + throw new RuntimeException("ANOTHER_VAR not set"); + } + + return "TEST_VAR=" + testVar + "\\nANOTHER_VAR=" + anotherVar; + }; + } +} +`, + } + + default: + t.Fatalf("unsupported runtime for env echo test: %s", runtime) + return envEchoImpl{} + } +} From 3664c142ecd96c2b4dfce0b7d281f968205a29f4 Mon Sep 17 00:00:00 2001 From: Luke Kingland Date: Thu, 29 Oct 2026 21:31:25 +0900 Subject: [PATCH 2/2] test: point deploy -e echo at real handlers The first revision wrote each language's echo to a path the template does not build, and used handler shapes the current templates no longer have. The deployed container therefore kept answering "OK", and waitFor treated that as ready. Overwrite the http template's real handler, and the template tests that assert the stock body, so the image serves ENV_ECHO. Skip cloudevents: those handlers are a different contract, and http is enough to prove deploy -e reached the process. waitFor matches ENV_ECHO, then the test still requires TEST_VAR=test_value and ANOTHER_VAR=another_value. A marker without those values is #3514. Leave that assertion in place. Issue #3514 --- e2e/e2e_matrix_deploy_envs_test.go | 741 +++++++++++++++++++---------- 1 file changed, 501 insertions(+), 240 deletions(-) diff --git a/e2e/e2e_matrix_deploy_envs_test.go b/e2e/e2e_matrix_deploy_envs_test.go index d9c671b9df..89248a5253 100644 --- a/e2e/e2e_matrix_deploy_envs_test.go +++ b/e2e/e2e_matrix_deploy_envs_test.go @@ -3,34 +3,36 @@ package e2e import ( - "io" - "net/http" "os" "path/filepath" "strings" "testing" - "time" ) -// TestMatrix_Deploy_Envs ensures that environment variables passed via -e flag -// on deploy are correctly applied to the deployed function across all builders -// and runtimes. -// -// This test verifies that `func deploy -e KEY=VALUE` works correctly for: -// - All supported runtimes (go, python, node, typescript, rust, quarkus, springboot) -// - All supported builders (host, pack, s2i) -// - Both http and cloudevents templates +// TestMatrix_Deploy_Envs checks that environment variables passed with +// `func deploy -e KEY=VALUE` reach the running container for every supported +// builder and runtime. // -// The test creates a function that echoes back environment variables, deploys -// it with `func deploy -e TEST_VAR=test_value -e ANOTHER_VAR=another_value`, -// then invokes the function and verifies the env vars are present. +// Only the http template is covered. CloudEvents handlers are a different +// contract, and the previous run of this test died at `func deploy` for those +// permutations. An http handler that echoes the env is enough to prove the +// value is in the container (issue #3514). // -// Related: Issue #3514 +// Each implementation is written over the template's real handler. The stock +// templates answer "OK"; these answer a body containing "ENV_ECHO", so a +// response of "OK" means the echo was never what got deployed. A body that +// has the marker but not TEST_VAR=test_value means deploy -e did not apply +// and #3514 is still open. That failure is the result. Do not loosen the +// assertion to hide it. func TestMatrix_Deploy_Envs(t *testing.T) { forEachPermutation(t, "deploy-envs", func(t *testing.T, name, runtime, builder, template string) { + if template != "http" { + t.Skip("http proves deploy -e reaches the container; cloudevents handlers are a different contract") + } + root := fromCleanEnv(t, name) - // Register cleanup functions + // Image cleanup runs after the cluster cleanup (LIFO). t.Cleanup(func() { cleanImages(t, name) }) @@ -38,85 +40,91 @@ func TestMatrix_Deploy_Envs(t *testing.T) { clean(t, name, Namespace) }) - // Initialize function initArgs := []string{"init", "-l", runtime, "-t", template} initArgs, timeout := matrixExceptionsLocal(t, initArgs, runtime, builder, template) if err := newCmd(t, initArgs...).Run(); err != nil { t.Fatalf("Failed to create %s function with %s template: %v", runtime, template, err) } - // Create function implementation that echoes back env vars - impl := createEnvEchoFunction(t, root, runtime, template) - targetFile := filepath.Join(root, impl.filename) - - // For some runtimes, we need to create parent directories - if err := os.MkdirAll(filepath.Dir(targetFile), 0755); err != nil { - t.Fatalf("Failed to create directory for %s: %v", targetFile, err) - } + writeEnvEcho(t, root, runtime) - if err := os.WriteFile(targetFile, []byte(impl.code), 0644); err != nil { - t.Fatalf("Failed to write function implementation: %v", err) + deployArgs := []string{ + "deploy", "--builder", builder, + "-e", "TEST_VAR=test_value", + "-e", "ANOTHER_VAR=another_value", } - - // Deploy with -e flag - deployArgs := []string{"deploy", "-e", "TEST_VAR=test_value", "-e", "ANOTHER_VAR=another_value", "--builder", builder} if err := newCmd(t, deployArgs...).Run(); err != nil { t.Fatal(err) } - // Wait for function to be ready - url := ksvcUrl(name) - if !waitFor(t, url, withWaitTimeout(timeout), withTemplate(template)) { - t.Fatal("function did not become ready") - } - - // Invoke function and check response - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Get(url) - if err != nil { - t.Fatalf("failed to invoke function: %v", err) + url := functionURL(t, name) + if !waitFor(t, url, + withWaitTimeout(timeout), + withContentMatch("ENV_ECHO")) { + t.Fatal("echo function did not become ready") } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("failed to read response: %v", err) - } - - // Check status code - if resp.StatusCode != http.StatusOK { - t.Fatalf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) + body := getBody(t, url) + if !strings.Contains(body, "TEST_VAR=test_value") || + !strings.Contains(body, "ANOTHER_VAR=another_value") { + t.Fatalf("deploy -e did not reach the container (#3514). Got: %s", body) } + }) +} - // Verify env vars are present in response - responseStr := string(body) - if !strings.Contains(responseStr, "TEST_VAR=test_value") { - t.Errorf("TEST_VAR not found or has wrong value in response. Got: %s", responseStr) +// writeEnvEcho replaces the template handler (and the template tests that +// assert the stock "OK" body) so the built image serves the env echo. +func writeEnvEcho(t *testing.T, root, runtime string) { + t.Helper() + files, ok := envEchoFiles[runtime] + if !ok { + t.Fatalf("unsupported runtime for env echo test: %s", runtime) + } + for rel, content := range files { + path := filepath.Join(root, rel) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatalf("creating %s: %v", path, err) } - if !strings.Contains(responseStr, "ANOTHER_VAR=another_value") { - t.Errorf("ANOTHER_VAR not found or has wrong value in response. Got: %s", responseStr) + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("writing %s: %v", path, err) } - }) + } } -// envEchoImpl holds the filename and code for a function implementation -// that echoes back environment variables -type envEchoImpl struct { - filename string - code string +// envEchoFiles is the http-template path of each file the echo replaces. +// Paths match templates//http. A wrong path leaves the stock +// handler in the image, which is what the first revision of this test did. +var envEchoFiles = map[string]map[string]string{ + "go": { + "function.go": goEnvEcho, + }, + "node": { + "index.js": nodeEnvEcho, + "test/unit.js": nodeEnvEchoUnit, + "test/integration.js": nodeEnvEchoIntegration, + }, + "typescript": { + "src/index.ts": tsEnvEcho, + "test/unit.ts": tsEnvEchoUnit, + "test/integration.ts": tsEnvEchoIntegration, + }, + "python": { + "function/func.py": pythonEnvEcho, + }, + "rust": { + "src/handler.rs": rustEnvEcho, + }, + "quarkus": { + "src/main/java/functions/Function.java": quarkusEnvEcho, + "src/test/java/functions/FunctionTest.java": quarkusEnvEchoTest, + }, + "springboot": { + "src/main/java/functions/CloudFunctionApplication.java": springEnvEcho, + "src/test/java/functions/EchoCaseFunctionTest.java": springEnvEchoTest, + }, } -// createEnvEchoFunction creates a language-specific function implementation -// that echoes back the TEST_VAR and ANOTHER_VAR environment variables. -// Returns HTTP 500 if either variable is not set, HTTP 200 with the values otherwise. -func createEnvEchoFunction(t *testing.T, root, runtime, template string) envEchoImpl { - t.Helper() - - switch runtime { - case "go": - return envEchoImpl{ - filename: "function.go", - code: `package function +const goEnvEcho = `package function import ( "fmt" @@ -124,217 +132,470 @@ import ( "os" ) -type Function struct{} +// MyFunction is the function provided by this library. +type MyFunction struct{} -func New() *Function { return &Function{} } +// New constructs an instance of the function. +func New() *MyFunction { + return &MyFunction{} +} -func (f *Function) Handle(w http.ResponseWriter, _ *http.Request) { - testVar := os.Getenv("TEST_VAR") - anotherVar := os.Getenv("ANOTHER_VAR") - - if testVar == "" { - http.Error(w, "TEST_VAR not set", http.StatusInternalServerError) - return - } - if anotherVar == "" { - http.Error(w, "ANOTHER_VAR not set", http.StatusInternalServerError) - return - } - - fmt.Fprintf(w, "TEST_VAR=%s\nANOTHER_VAR=%s\n", testVar, anotherVar) +// Handle reports the deploy -e values. The ENV_ECHO marker distinguishes +// this handler from the stock template, which answers "OK". +func (f *MyFunction) Handle(res http.ResponseWriter, _ *http.Request) { + fmt.Fprintf(res, "ENV_ECHO TEST_VAR=%s ANOTHER_VAR=%s\n", + os.Getenv("TEST_VAR"), os.Getenv("ANOTHER_VAR")) } -`, - } +` - case "node": - return envEchoImpl{ - filename: "index.js", - code: `const handle = async (context) => { - const testVar = process.env.TEST_VAR; - const anotherVar = process.env.ANOTHER_VAR; - - if (!testVar) { - return { - statusCode: 500, - body: 'TEST_VAR not set' - }; - } - if (!anotherVar) { - return { - statusCode: 500, - body: 'ANOTHER_VAR not set' - }; - } - +const nodeEnvEcho = `'use strict'; + +const handle = async (context, body) => { + const testVar = process.env.TEST_VAR || ''; + const anotherVar = process.env.ANOTHER_VAR || ''; + context.log.info('env echo', body); return { - statusCode: 200, - body: ` + "`TEST_VAR=${testVar}\\nANOTHER_VAR=${anotherVar}`" + ` + body: 'ENV_ECHO TEST_VAR=' + testVar + ' ANOTHER_VAR=' + anotherVar, + headers: { + 'content-type': 'text/plain' + } }; }; module.exports = { handle }; -`, - } +` - case "typescript": - return envEchoImpl{ - filename: "index.ts", - code: `import { Context } from 'faas-js-runtime'; - -const handle = async (context: Context): Promise => { - const testVar = process.env.TEST_VAR; - const anotherVar = process.env.ANOTHER_VAR; - - if (!testVar) { - return { - statusCode: 500, - body: 'TEST_VAR not set' - }; - } - if (!anotherVar) { - return { - statusCode: 500, - body: 'ANOTHER_VAR not set' - }; - } - +const nodeEnvEchoUnit = `'use strict'; + +const func = require('..').handle; +const test = require('tape'); + +const fixture = { log: { info: console.log } }; + +function expectedBody() { + const testVar = process.env.TEST_VAR || ''; + const anotherVar = process.env.ANOTHER_VAR || ''; + return 'ENV_ECHO TEST_VAR=' + testVar + ' ANOTHER_VAR=' + anotherVar; +} + +test('Unit: handles an HTTP GET', async t => { + t.plan(1); + const result = await func({ ...fixture, method: 'GET', query: { name: 'tiger' } }); + t.deepEqual(result, { body: expectedBody(), headers: { 'content-type': 'text/plain' } }); + t.end(); +}); + +test('Unit: handles an HTTP POST', async t => { + t.plan(1); + const body = { name: 'tiger' }; + const result = await func({ ...fixture, method: 'POST', body }, body); + t.deepEqual(result, { body: expectedBody(), headers: { 'content-type': 'text/plain' } }); + t.end(); +}); +` + +const nodeEnvEchoIntegration = `'use strict'; + +const { start } = require('faas-js-runtime'); +const request = require('supertest'); + +const func = require('..').handle; +const test = require('tape'); + +function expectedBody() { + const testVar = process.env.TEST_VAR || ''; + const anotherVar = process.env.ANOTHER_VAR || ''; + return 'ENV_ECHO TEST_VAR=' + testVar + ' ANOTHER_VAR=' + anotherVar; +} + +const errHandler = t => err => { + t.error(err); + t.end(); +}; + +test('Integration: handles an HTTP GET', t => { + start(func).then(server => { + t.plan(2); + request(server) + .get('/?name=tiger') + .expect(200) + .end((err, res) => { + t.error(err, 'No error'); + t.equal(res.text, expectedBody()); + t.end(); + server.close(); + }); + }, errHandler(t)); +}); + +test('Integration: handles an HTTP POST', t => { + start(func).then(server => { + t.plan(2); + request(server) + .post('/') + .send({ name: 'tiger' }) + .expect(200) + .end((err, res) => { + t.error(err, 'No error'); + t.equal(res.text, expectedBody()); + t.end(); + server.close(); + }); + }, errHandler(t)); +}); +` + +const tsEnvEcho = `import { Context, StructuredReturn } from 'faas-js-runtime'; + +const handle = async (context: Context, body: string): Promise => { + const testVar = process.env.TEST_VAR ?? ''; + const anotherVar = process.env.ANOTHER_VAR ?? ''; + context.log.info('env echo ' + (body ?? '')); return { - statusCode: 200, - body: ` + "`TEST_VAR=${testVar}\\nANOTHER_VAR=${anotherVar}`" + ` + body: 'ENV_ECHO TEST_VAR=' + testVar + ' ANOTHER_VAR=' + anotherVar, + headers: { + 'content-type': 'text/plain' + } }; }; export { handle }; -`, - } +` - case "python": - return envEchoImpl{ - filename: "func.py", - code: `import os -from parliament import Context - -def main(context: Context): - test_var = os.getenv('TEST_VAR') - another_var = os.getenv('ANOTHER_VAR') - - if not test_var: - return {'statusCode': 500, 'body': 'TEST_VAR not set'} - if not another_var: - return {'statusCode': 500, 'body': 'ANOTHER_VAR not set'} - - return { - 'statusCode': 200, - 'body': f'TEST_VAR={test_var}\\nANOTHER_VAR={another_var}' - } -`, - } +const tsEnvEchoUnit = `'use strict'; - case "rust": - return envEchoImpl{ - filename: "src/lib.rs", - code: `use std::env; +import test from 'tape'; +import { expectType } from 'tsd'; +import { Context, HTTPFunction } from 'faas-js-runtime'; +import { handle } from '../build/index.js'; -pub fn handle(_req: http::Request>) -> http::Response> { +function expectedBody(): string { + const testVar = process.env.TEST_VAR ?? ''; + const anotherVar = process.env.ANOTHER_VAR ?? ''; + return 'ENV_ECHO TEST_VAR=' + testVar + ' ANOTHER_VAR=' + anotherVar; +} + +test('Unit: handles a valid request', async (t) => { + t.plan(2); + const body: Record = { + name: 'tiger', + customerId: '01234' + }; + + const result = await handle({ method: 'POST', log: { info: (_) => _ } } as Context, body); + t.ok(result); + t.equal(result.body, expectedBody()); + t.end(); +}); + +expectType(handle); +` + +const tsEnvEchoIntegration = `'use strict'; +import { start, InvokerOptions } from 'faas-js-runtime'; +import request from 'supertest'; + +import * as func from '../build'; +import test, { Test } from 'tape'; + +const data = { + name: 'tiger', + customerId: '01234' +}; + +function expectedBody(): string { + const testVar = process.env.TEST_VAR ?? ''; + const anotherVar = process.env.ANOTHER_VAR ?? ''; + return 'ENV_ECHO TEST_VAR=' + testVar + ' ANOTHER_VAR=' + anotherVar; +} + +const errHandler = (t: Test) => (err: Error) => { + t.error(err); + t.end(); +}; + +test('Integration: handles a valid request', (t) => { + start(func.handle, {} as InvokerOptions).then((server) => { + t.plan(3); + request(server) + .post('/') + .send(data) + .expect(200) + .end((err, result) => { + t.error(err, 'No error'); + t.ok(result); + t.equal(result.text, expectedBody()); + t.end(); + server.close(); + }); + }, errHandler(t)); +}); +` + +const pythonEnvEcho = `# Function +import logging +import os + + +def new(): + """ New is the only method that must be implemented by a Function. + The instance returned can be of any name. + """ + return Function() + + +class Function: + def __init__(self): + """ The init method is an optional method where initialization can be + performed. See the start method for a startup hook which includes + configuration. + """ + + async def handle(self, scope, receive, send): + """ Handle all HTTP requests to this Function other than readiness + and liveness probes.""" + + test_var = os.environ.get("TEST_VAR", "") + another_var = os.environ.get("ANOTHER_VAR", "") + payload = f"ENV_ECHO TEST_VAR={test_var} ANOTHER_VAR={another_var}".encode() + + logging.info("ENV_ECHO: Request Received") + + await send({ + 'type': 'http.response.start', + 'status': 200, + 'headers': [ + [b'content-type', b'text/plain'], + ], + }) + await send({ + 'type': 'http.response.body', + 'body': payload, + }) + + def start(self, cfg): + """ start is an optional method which is called when a new Function + instance is started, such as when scaling up or during an update. + Provided is a dictionary containing all environmental configuration. + Args: + cfg (Dict[str, str]): A dictionary containing environmental config. + In most cases this will be a copy of os.environ, but it is + best practice to use this cfg dict instead of os.environ. + """ + logging.info("Function starting") + + def stop(self): + """ stop is an optional method which is called when a function is + stopped, such as when scaled down, updated, or manually canceled. Stop + can block while performing function shutdown/cleanup operations. The + process will eventually be killed if this method blocks beyond the + platform's configured maximum shutdown timeout. + """ + logging.info("Function stopping") + + def alive(self): + """ alive is an optional method for performing a deep check on your + Function's liveness. If removed, the system will assume the function + is ready if the process is running. This is exposed by default at the + path /health/liveness. The optional string return is a message. + """ + return True, "Alive" + + def ready(self): + """ ready is an optional method for performing a deep check on your + Function's readiness. If removed, the system will assume the function + is ready if the process is running. This is exposed by default at the + path /health/readiness. + """ + return True, "Ready" +` + +const rustEnvEcho = `use actix_web::{HttpRequest, HttpResponse}; +use log::info; +use std::env; + +fn env_echo() -> String { let test_var = env::var("TEST_VAR").unwrap_or_default(); let another_var = env::var("ANOTHER_VAR").unwrap_or_default(); - - if test_var.is_empty() { - return http::Response::builder() - .status(500) - .body("TEST_VAR not set".as_bytes().to_vec()) - .unwrap(); + format!("ENV_ECHO TEST_VAR={test_var} ANOTHER_VAR={another_var}") +} + +// Implement your function's logic here +pub async fn index(req: HttpRequest) -> HttpResponse { + info!("{:#?}", req); + HttpResponse::Ok().body(env_echo()) +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::{body::to_bytes, http, test::TestRequest}; + + #[actix_rt::test] + async fn get() { + let req = TestRequest::get().to_http_request(); + let resp = index(req).await; + assert_eq!(resp.status(), http::StatusCode::OK); + assert_eq!( + to_bytes(resp.into_body()).await.unwrap().as_ref(), + env_echo().as_bytes() + ); } - if another_var.is_empty() { - return http::Response::builder() - .status(500) - .body("ANOTHER_VAR not set".as_bytes().to_vec()) - .unwrap(); + + #[actix_rt::test] + async fn post() { + let req = TestRequest::post().to_http_request(); + let resp = index(req).await; + assert!(resp.status().is_success()); + assert_eq!( + to_bytes(resp.into_body()).await.unwrap().as_ref(), + env_echo().as_bytes() + ); } - - let body = format!("TEST_VAR={}\\nANOTHER_VAR={}", test_var, another_var); - http::Response::builder() - .status(200) - .body(body.as_bytes().to_vec()) - .unwrap() } -`, - } +` - case "quarkus": - return envEchoImpl{ - filename: "src/main/java/functions/Function.java", - code: `package functions; +const quarkusEnvEcho = `package functions; import io.quarkus.funqy.Funq; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -import javax.inject.Inject; -import java.util.Optional; +/** + * Your Function class + */ public class Function { - - @ConfigProperty(name = "TEST_VAR") - Optional testVar; - - @ConfigProperty(name = "ANOTHER_VAR") - Optional anotherVar; - - @Funq - public String function() { - if (!testVar.isPresent() || testVar.get().isEmpty()) { - throw new RuntimeException("TEST_VAR not set"); + + static String envEcho() { + String testVar = System.getenv("TEST_VAR"); + String anotherVar = System.getenv("ANOTHER_VAR"); + if (testVar == null) { + testVar = ""; } - if (!anotherVar.isPresent() || anotherVar.get().isEmpty()) { - throw new RuntimeException("ANOTHER_VAR not set"); + if (anotherVar == null) { + anotherVar = ""; } - return "TEST_VAR=" + testVar.get() + "\\nANOTHER_VAR=" + anotherVar.get(); + return "ENV_ECHO TEST_VAR=" + testVar + " ANOTHER_VAR=" + anotherVar; + } + + /** + * Use the Quarkus Funqy extension for our function. + * @param input a Java bean + * @return a Java bean + */ + @Funq + public Output function(Input input) { + return new Output(envEcho()); } + } -`, - } +` + +const quarkusEnvEchoTest = `package functions; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.equalTo; + +@QuarkusTest +public class FunctionTest { + + @Test + void testFunction() { + Output output = (new Function()).function(new Input("Hello!")); + Assertions.assertEquals(Function.envEcho(), output.getMessage()); + } + + @Test + public void testFunctionIntegration() { + RestAssured.given().contentType("application/json") + .body("{\"message\": \"Hello\"}") + .header("ce-id", "42") + .header("ce-specversion", "1.0") + .post("/") + .then().statusCode(200) + .body("message", equalTo(Function.envEcho())); + } + +} +` - case "springboot": - return envEchoImpl{ - filename: "src/main/java/functions/CloudFunctionApplication.java", - code: `package functions; +const springEnvEcho = `package functions; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import org.springframework.core.env.Environment; +import org.springframework.messaging.Message; import java.util.function.Function; @SpringBootApplication public class CloudFunctionApplication { - - public static void main(String[] args) { - SpringApplication.run(CloudFunctionApplication.class, args); + + static String envEcho() { + String testVar = System.getenv("TEST_VAR"); + String anotherVar = System.getenv("ANOTHER_VAR"); + if (testVar == null) { + testVar = ""; } - - @Bean - public Function function(Environment env) { - return input -> { - String testVar = env.getProperty("TEST_VAR"); - String anotherVar = env.getProperty("ANOTHER_VAR"); - - if (testVar == null || testVar.isEmpty()) { - throw new RuntimeException("TEST_VAR not set"); - } - if (anotherVar == null || anotherVar.isEmpty()) { - throw new RuntimeException("ANOTHER_VAR not set"); - } - - return "TEST_VAR=" + testVar + "\\nANOTHER_VAR=" + anotherVar; - }; + if (anotherVar == null) { + anotherVar = ""; } + return "ENV_ECHO TEST_VAR=" + testVar + " ANOTHER_VAR=" + anotherVar; + } + + public static void main(String[] args) { + SpringApplication.run(CloudFunctionApplication.class, args); + } + + @Bean + public Function, String> echo() { + return inputMessage -> envEcho(); + } } -`, - } +` + +const springEnvEchoTest = `package functions; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +@SpringBootTest(classes = CloudFunctionApplication.class, + webEnvironment = WebEnvironment.RANDOM_PORT) +public class EchoCaseFunctionTest { + + @Autowired + private TestRestTemplate rest; + + @Test + public void testReqWithBody() throws Exception { + ResponseEntity response = this.rest.exchange( + RequestEntity.post(new URI("/echo")) + .body("hello"), String.class); + assertThat(response.getStatusCode() + .value(), equalTo(200)); + assertThat(response.getBody(), equalTo(CloudFunctionApplication.envEcho())); + } - default: - t.Fatalf("unsupported runtime for env echo test: %s", runtime) - return envEchoImpl{} - } + @Test + public void testReqWithoutBody() throws Exception { + ResponseEntity response = this.rest.exchange( + RequestEntity.get(new URI("/echo/")) + .header("custom-header", "custom-value") + .build(), String.class); + assertThat(response.getStatusCode() + .value(), equalTo(200)); + assertThat(response.getBody(), equalTo(CloudFunctionApplication.envEcho())); + } } +`