Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ help:
@echo " test-integration-csharp - Run C# integration tests"
@echo " test-integration-go - Run Go integration tests"
@echo " test-integration-nodejs - Run NodeJS integration tests"
@echo " test-integration-react - Run React integration tests"
@echo " generate - Generate all code (API clients, docs, schema)"
@echo " generate-api - Generate API clients from OpenAPI specs"
@echo " generate-docs - Generate documentation"
Expand Down Expand Up @@ -67,6 +68,11 @@ test-integration-angular:
@echo "Running Angular integration test with Dagger..."
@go run ./test/integration/cmd/angular/run.go

.PHONY: test-integration-react
test-integration-react:
@echo "Running React integration test with Dagger..."
@go run ./test/integration/cmd/react/run.go

.PHONY: test-integration
test-integration:
@echo "Running all integration tests with Dagger..."
Expand Down
98 changes: 98 additions & 0 deletions test/integration/cmd/react/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package main

import (
"context"
"fmt"
"os"
"path/filepath"

"dagger.io/dagger"
"github.com/open-feature/cli/test/integration"
)

// Test implements the integration test for the React generator
type Test struct {
// ProjectDir is the absolute path to the root of the project
ProjectDir string
// TestDir is the absolute path to the test directory
TestDir string
}

// New creates a new Test
func New(projectDir, testDir string) *Test {
return &Test{
ProjectDir: projectDir,
TestDir: testDir,
}
}

// Run executes the React integration test using Dagger
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
// Source code container
source := client.Host().Directory(t.ProjectDir)
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
Include: []string{"package.json", "tsconfig.json", "src/**/*.ts", "src/**/*.tsx"},
})

// Build the CLI in a Go container
cli := client.Container().
From("golang:1.24.3-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
Comment on lines +39 to +40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's good practice to specify the exact version of golang and alpine to ensure consistent builds across different environments and over time. This prevents unexpected build failures if a new version introduces breaking changes.

Suggested change
From("golang:1.24.3-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).
From("golang:1.24.3-alpine3.19").

WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "mod", "tidy"}).
WithExec([]string{"go", "mod", "download"}).
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})
Comment on lines +40 to +45

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's good practice to specify the exact version of golangci-lint or go tools in Dagger containers to ensure reproducible builds and avoid unexpected changes due to upstream updates. While golang:1.24-alpine is specific, the go mod tidy and go mod download commands could potentially pull different versions of dependencies over time. Consider pinning specific versions for critical tools or dependencies.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I've pinned the Go version to golang:1.24.3-alpine for reproducible builds. This ensures the exact same Go toolchain is used across all test runs. ✅


// Generate React client
generated := cli.WithExec([]string{
"./cli", "generate", "react",
"--manifest=/src/sample/sample_manifest.json",
"--output=/tmp/generated",
})

// Get generated files
generatedFiles := generated.Directory("/tmp/generated")

// Create the React test container
reactContainer := client.Container().
From("node:20.18.1-alpine3.19").
WithDirectory("/app", testFiles).
WithDirectory("/app/src/generated", generatedFiles).
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "run", "build"}).
WithExec([]string{"node", "dist/test.js"})
Comment on lines +58 to +64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The indentation for this Dagger pipeline definition is inconsistent, which affects readability. The From call on line 59 is not indented correctly. It's a good practice to run a Go formatter (gofmt) on new files to ensure consistent styling.

Suggested change
reactContainer := client.Container().
From("node:20.18.1-alpine3.19").
WithDirectory("/app", testFiles).
WithDirectory("/app/src/generated", generatedFiles).
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "run", "build"}).
WithExec([]string{"node", "dist/test.js"})
reactContainer := client.Container().
From("node:20.18.1-alpine3.19").
WithDirectory("/app", testFiles).
WithDirectory("/app/src/generated", generatedFiles).
WithExec([]string{"npm", "install"}).
WithExec([]string{"npm", "run", "build"}).
WithExec([]string{"node", "dist/test.js"})


return reactContainer, nil
}

// Name returns the name of the integration test
func (t *Test) Name() string {
return "react"
}

func main() {
ctx := context.Background()

// Get project root
projectDir, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
os.Exit(1)
}

// Get test directory
testDir, err := filepath.Abs(filepath.Join(projectDir, "test/react-integration"))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err)
os.Exit(1)
}

// Create and run the React integration test
test := New(projectDir, testDir)

if err := integration.RunTest(ctx, test); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
52 changes: 18 additions & 34 deletions test/integration/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,29 @@ import (
)

func main() {
// Run the language-specific tests
fmt.Println("=== Running all integration tests ===")

// Run the C# integration test
csharpCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/csharp")
csharpCmd.Stdout = os.Stdout
csharpCmd.Stderr = os.Stderr
if err := csharpCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running C# integration test: %v\n", err)
os.Exit(1)
tests := []struct {
name string
path string
}{
{"C#", "github.com/open-feature/cli/test/integration/cmd/csharp"},
{"Go", "github.com/open-feature/cli/test/integration/cmd/go"},
{"NodeJS", "github.com/open-feature/cli/test/integration/cmd/nodejs"},
{"Angular", "github.com/open-feature/cli/test/integration/cmd/angular"},
{"React", "github.com/open-feature/cli/test/integration/cmd/react"},
}

// Run the Go integration test
goCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/go")
goCmd.Stdout = os.Stdout
goCmd.Stderr = os.Stderr
if err := goCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running Go integration test: %v\n", err)
os.Exit(1)
for _, test := range tests {
fmt.Printf("--- Running %s integration test ---\n", test.name)
cmd := exec.Command("go", "run", test.path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running %s integration test: %v\n", test.name, err)
os.Exit(1)
}
}
// Run the nodejs test
nodeCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/nodejs")
nodeCmd.Stdout = os.Stdout
nodeCmd.Stderr = os.Stderr
if err := nodeCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running nodejs integration test: %v\n", err)
os.Exit(1)
}

// Run the Angular integration test
angularCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/angular")
angularCmd.Stdout = os.Stdout
angularCmd.Stderr = os.Stderr
if err := angularCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running Angular integration test: %v\n", err)
os.Exit(1)
}

// Add more tests here as they are available

fmt.Println("=== All integration tests passed successfully ===")
}
19 changes: 19 additions & 0 deletions test/react-integration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "cli-react-integration-test",
"version": "1.0.0",
"description": "Integration test for OpenFeature CLI React generator",
"scripts": {
"build": "tsc",
"test": "node dist/test.js"
},
"dependencies": {
"@openfeature/react-sdk": "1.0.2",
"@openfeature/server-sdk": "1.34.0",
"react": "18.2.0"
},
"devDependencies": {
"@types/node": "20.17.10",
"@types/react": "18.2.79",
"typescript": "5.3.3"
}
}
53 changes: 53 additions & 0 deletions test/react-integration/src/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as generated from './generated';

async function main() {
try {
// Validate that all generated exports exist and have the expected structure
const flags = [
{ name: 'EnableFeatureA', flag: generated.EnableFeatureA, expectedKey: 'enableFeatureA' },
{ name: 'DiscountPercentage', flag: generated.DiscountPercentage, expectedKey: 'discountPercentage' },
{ name: 'GreetingMessage', flag: generated.GreetingMessage, expectedKey: 'greetingMessage' },
{ name: 'UsernameMaxLength', flag: generated.UsernameMaxLength, expectedKey: 'usernameMaxLength' },
{ name: 'ThemeCustomization', flag: generated.ThemeCustomization, expectedKey: 'themeCustomization' },
];

for (const { name, flag, expectedKey } of flags) {
// Validate the flag object has the expected properties
if (typeof flag !== 'object' || flag === null) {
throw new Error(`${name} is not an object`);
}

// Check for getKey method
if (typeof flag.getKey !== 'function') {
throw new Error(`${name}.getKey is not a function`);
}

const key = flag.getKey();
console.log(`${name} flag key:`, key);

if (key !== expectedKey) {
throw new Error(`${name} has incorrect key. Expected '${expectedKey}', but got '${key}'.`);
}

// Check for useFlag hook
if (typeof flag.useFlag !== 'function') {
throw new Error(`${name}.useFlag is not a function`);
}

// Check for useFlagWithDetails hook
if (typeof flag.useFlagWithDetails !== 'function') {
throw new Error(`${name}.useFlagWithDetails is not a function`);
}
}

console.log('All generated React hooks are properly structured!');
console.log('Generated React code compiles successfully!');
process.exit(0);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.error('Error:', message);
process.exit(1);
}
}

main();
18 changes: 18 additions & 0 deletions test/react-integration/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"lib": ["ES2021"],
"jsx": "react",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}