package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"lunar/engine/utils/environment"
"net/http"
"net/http/httptest"
"os"
"path"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
wd, err := os.Getwd()
if err != nil {
panic(err)
}
procPath := path.Join(path.Dir(wd), "lunar-engine/streams/processors")
os.Setenv("LUNAR_PROXY_PROCESSORS_DIRECTORY", procPath)
os.Setenv("LUNAR_SPOE_PROCESSING_TIMEOUT_SEC", "61")
code := m.Run()
os.RemoveAll(path.Join(".", DefaultRoot))
os.Exit(code)
}
func TestGetValidatorPort(t *testing.T) {
t.Run("Default Port", func(t *testing.T) {
os.Unsetenv(ValidatorPortEnvVar)
port := GetValidatorPort()
require.Equal(t, DefaultPort, port)
})
t.Run("Custom Port", func(t *testing.T) {
os.Setenv(ValidatorPortEnvVar, "9090")
defer os.Unsetenv(ValidatorPortEnvVar)
port := GetValidatorPort()
require.Equal(t, "9090", port)
})
}
func TestWriteBase64File(t *testing.T) {
content := "test content"
encoded := base64.StdEncoding.EncodeToString([]byte(content))
tempFile := filepath.Join(os.TempDir(), "testfile.yaml")
defer os.Remove(tempFile)
err := writeBase64File(encoded, tempFile)
require.NoError(t, err)
data, err := os.ReadFile(tempFile)
require.NoError(t, err)
require.Equal(t, content, string(data))
}
func TestValidation(t *testing.T) {
testCases := []struct {
name string
testFolder string
useTestFolder bool // if true, the test folder will be used as the root folder
validationFn func(t *testing.T, input *ValidationInput, result *ValidationResult)
}{
{
name: "Valid",
testFolder: "valid",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 2)
require.Len(t, input.PathParams, 1)
require.Len(t, input.Quotas, 2)
require.NotEmpty(t, input.GatewayConfig)
require.True(t, result.Success, result.Message)
},
},
{
name: "Valid with ready folder",
testFolder: "valid",
useTestFolder: true,
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 0)
require.Len(t, input.PathParams, 0)
require.Len(t, input.Quotas, 0)
require.Empty(t, input.GatewayConfig)
require.NotEmpty(t, input.FolderPath)
require.True(t, result.Success, result.Message)
},
},
{
name: "Invalid queue processor configuration - queue ttl more than LUNAR_SPOE_PROCESSING_TIMEOUT_SEC",
testFolder: "invalid-queue-processor-config",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 2)
require.Len(t, input.Quotas, 2)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
require.Contains(t, result.Message, "processing timeout (1m1s) is less than queue TTL (2m0s)")
},
},
{
name: "Invalid gateway configuration",
testFolder: "invalid-gateway-config",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 0)
require.Len(t, input.Quotas, 0)
require.Len(t, input.PathParams, 0)
require.NotEmpty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
require.Contains(t, result.Message, "failed to validate gateway config")
},
},
{
name: "Invalid Quota",
testFolder: "invalid-quota",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 1)
require.Len(t, input.Quotas, 1)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
},
},
{
name: "Quota With Same Provider Across Multiple Files",
testFolder: "quota-with-same-provider-across-multiple-files",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 0)
require.Len(t, input.Quotas, 2)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
},
},
{
name: "Quota With Same Provider Across Multiple Files - Internal Filter",
testFolder: "quota-with-same-provider-across-multiple-files-internal-filter",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 0)
require.Len(t, input.Quotas, 2)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
},
},
{
name: "Invalid Flow - no request",
testFolder: "no-request-section",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 1)
require.Len(t, input.Quotas, 0)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
},
},
{
name: "Invalid Flow - no response",
testFolder: "no-response-section",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 1)
require.Len(t, input.Quotas, 0)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
},
},
{
name: "Invalid flow yaml",
testFolder: "invalid-yaml-flow",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 1)
require.Len(t, input.Quotas, 0)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
},
},
{
name: "Invalid quota with no filter",
testFolder: "quota-with-no-filter",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 0)
require.Len(t, input.Quotas, 1)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
},
},
{
name: "Invalid flow - invalid processor",
testFolder: "flow-with-invalid-processor",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 2)
require.Len(t, input.Quotas, 2)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
require.Contains(t, result.Message, "failed to create processor")
},
},
{
name: "Invalid flow - processor with duplicate keys",
testFolder: "flow-with-processor-duplicate-params",
validationFn: func(t *testing.T, input *ValidationInput, result *ValidationResult) {
require.Len(t, input.Flows, 2)
require.Len(t, input.Quotas, 2)
require.Len(t, input.PathParams, 0)
require.Empty(t, input.GatewayConfig)
require.False(t, result.Success, result.Message)
require.Contains(t, result.Message, "duplicate key")
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testFolder := filepath.Join("test-cases", tc.testFolder)
var input *ValidationInput
if tc.useTestFolder {
input = &ValidationInput{
FolderPath: testFolder,
}
} else {
input = loadTestCase(t, testFolder)
}
body, err := json.Marshal(input)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/validate-flows", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
validateFlowsHandler(w, req)
resp := w.Result()
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
var result ValidationResult
err = json.NewDecoder(resp.Body).Decode(&result)
require.NoError(t, err)
tc.validationFn(t, input, &result)
})
}
}
func loadTestCase(t *testing.T, testFolder string) *ValidationInput {
valInput := &ValidationInput{}
valInput.Flows = loadFiles(t, testFolder, environment.FlowsFolder)
valInput.PathParams = loadFiles(t, testFolder, environment.PathParamsFolder)
valInput.Quotas = loadFiles(t, testFolder, environment.QuotasFolder)
gatewayConfig, _ := os.ReadFile(environment.GetCustomGatewayConfigPath(testFolder))
if len(gatewayConfig) != 0 {
valInput.GatewayConfig = base64.StdEncoding.EncodeToString(gatewayConfig)
}
return valInput
}
func loadFiles(t *testing.T, root, subFolder string) []string {
dirEntries, err := os.ReadDir(filepath.Join(root, subFolder))
if err != nil {
return nil
}
var files []string
for _, entry := range dirEntries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
continue
}
content, err := os.ReadFile(filepath.Join(root, subFolder, entry.Name()))
require.NoError(t, err)
// Convert the content to Base64
encoded := base64.StdEncoding.EncodeToString(content)
files = append(files, encoded)
}
return files
}