diff --git a/Makefile b/Makefile index b889d85..2b05c40 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ help: @echo " test-integration - Run all integration tests" @echo " test-integration-csharp - Run C# integration tests" @echo " test-integration-go - Run Go integration tests" + @echo " test-integration-nestjs - Run NestJS integration tests" @echo " test-integration-nodejs - Run NodeJS integration tests" @echo " generate - Generate all code (API clients, docs, schema)" @echo " generate-api - Generate API clients from OpenAPI specs" @@ -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-nestjs +test-integration-nestjs: + @echo "Running NestJS integration test with Dagger..." + @go run ./test/integration/cmd/nestjs/run.go + .PHONY: test-integration test-integration: @echo "Running all integration tests with Dagger..." diff --git a/test/integration/cmd/nestjs/run.go b/test/integration/cmd/nestjs/run.go new file mode 100644 index 0000000..126c3bf --- /dev/null +++ b/test/integration/cmd/nestjs/run.go @@ -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 NestJS 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 NestJS 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"}, + }) + + // Build the CLI + cli := client.Container(). + From("golang:1.24-alpine"). + WithExec([]string{"apk", "add", "--no-cache", "git"}). + WithDirectory("/src", source). + WithWorkdir("/src"). + WithExec([]string{"go", "mod", "download"}). + WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"}) + + // Generate NestJS client + generated := cli.WithExec([]string{ + "./cli", "generate", "nestjs", + "--manifest=/src/sample/sample_manifest.json", + "--output=/tmp/generated", + }) + + // Get generated files + generatedFiles := generated.Directory("/tmp/generated") + + // Test NestJS compilation with the generated files + nestjsContainer := client.Container(). + From("node:20-alpine"). + WithWorkdir("/app"). + WithDirectory("/app", testFiles). + WithDirectory("/app/src/generated", generatedFiles). + WithExec([]string{"npm", "install"}). + WithExec([]string{"npm", "run", "build"}). + WithExec([]string{"node", "dist/main.js"}) + + return nestjsContainer, nil +} + +// Name returns the name of the integration test +func (t *Test) Name() string { + return "nestjs" +} + +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/nestjs-integration")) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err) + os.Exit(1) + } + + // Create and run the NestJS 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) + } +} diff --git a/test/integration/cmd/run.go b/test/integration/cmd/run.go index 5923c26..eae4e7a 100644 --- a/test/integration/cmd/run.go +++ b/test/integration/cmd/run.go @@ -6,46 +6,30 @@ import ( "os/exec" ) -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) +// runIntegrationTest runs a single integration test for the specified language +func runIntegrationTest(language string) error { + cmd := exec.Command("go", "run", fmt.Sprintf("github.com/open-feature/cli/test/integration/cmd/%s", language)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("error running %s integration test: %w", language, err) } + return nil +} - // 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) - } - // 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) - } +func main() { + // List of all integration tests to run + tests := []string{"csharp", "go", "nodejs", "angular", "nestjs"} - // 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) - } + fmt.Println("=== Running all integration tests ===") - // Add more tests here as they are available + // Run each integration test + for _, test := range tests { + if err := runIntegrationTest(test); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + } fmt.Println("=== All integration tests passed successfully ===") } diff --git a/test/nestjs-integration/package.json b/test/nestjs-integration/package.json new file mode 100644 index 0000000..528971e --- /dev/null +++ b/test/nestjs-integration/package.json @@ -0,0 +1,21 @@ +{ + "name": "cli-nestjs-integration-test", + "version": "1.0.0", + "description": "Integration test for OpenFeature CLI NestJS generator", + "scripts": { + "build": "tsc", + "start": "node dist/main.js" + }, + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@openfeature/nestjs-sdk": "^0.7.0", + "@openfeature/server-sdk": "^1.34.0", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.3.0" + } +} diff --git a/test/nestjs-integration/src/main.ts b/test/nestjs-integration/src/main.ts new file mode 100644 index 0000000..6cc7e58 --- /dev/null +++ b/test/nestjs-integration/src/main.ts @@ -0,0 +1,184 @@ +import { NestFactory } from '@nestjs/core'; +import { Module, Injectable } from '@nestjs/common'; +import { OpenFeatureModule, OPENFEATURE_CLIENT } from '@openfeature/nestjs-sdk'; +import { InMemoryProvider, Client } from '@openfeature/server-sdk'; +import * as generated from './generated/openfeature'; +import { GeneratedOpenFeatureModule } from './generated/openfeature-module'; + +// Type definition for theme customization object +interface ThemeCustomization { + primaryColor: string; + secondaryColor: string; +} + +// Service that uses generated decorators to test NestJS-specific functionality +@Injectable() +class TestService { + constructor( + @generated.EnableFeatureA() private enableFeatureA: boolean, + @generated.DiscountPercentage() private discountPercentage: number, + @generated.GreetingMessage() private greetingMessage: string, + @generated.UsernameMaxLength() private usernameMaxLength: number, + @generated.ThemeCustomization() private themeCustomization: ThemeCustomization, + ) {} + + getFlags() { + return { + enableFeatureA: this.enableFeatureA, + discountPercentage: this.discountPercentage, + greetingMessage: this.greetingMessage, + usernameMaxLength: this.usernameMaxLength, + themeCustomization: this.themeCustomization, + }; + } +} + +@Module({ + imports: [ + GeneratedOpenFeatureModule.forRoot({ + provider: new InMemoryProvider({ + discountPercentage: { + disabled: false, + variants: { + default: 0.15, + }, + defaultVariant: 'default', + }, + enableFeatureA: { + disabled: false, + variants: { + default: false, + }, + defaultVariant: 'default', + }, + greetingMessage: { + disabled: false, + variants: { + default: 'Hello there!', + }, + defaultVariant: 'default', + }, + usernameMaxLength: { + disabled: false, + variants: { + default: 50, + }, + defaultVariant: 'default', + }, + themeCustomization: { + disabled: false, + variants: { + default: { + primaryColor: '#007bff', + secondaryColor: '#6c757d', + }, + }, + defaultVariant: 'default', + }, + }), + }), + ], + providers: [TestService], +}) +class AppModule {} + +// Test NestJS decorators by getting flags from the service +function testNestJSDecorators(testService: TestService): void { + const flagsFromDecorators = testService.getFlags(); + console.log('Flags from NestJS decorators:'); + console.log(' enableFeatureA:', flagsFromDecorators.enableFeatureA); + console.log(' discountPercentage:', flagsFromDecorators.discountPercentage.toFixed(2)); + console.log(' greetingMessage:', flagsFromDecorators.greetingMessage); + console.log(' usernameMaxLength:', flagsFromDecorators.usernameMaxLength); + console.log(' themeCustomization:', flagsFromDecorators.themeCustomization); +} + +// Test direct flag evaluation using the generated client methods +async function testDirectFlagEvaluation(client: Client): Promise { + console.log('\nDirect flag evaluation:'); + + // Boolean flag + const enableFeatureA = await generated.EnableFeatureA.value(client, {}); + console.log(' enableFeatureA:', enableFeatureA); + + const enableFeatureADetails = await generated.EnableFeatureA.valueWithDetails(client, {}); + if (enableFeatureADetails.errorCode) { + throw new Error('Error evaluating boolean flag'); + } + + // Number flag + const discount = await generated.DiscountPercentage.value(client, {}); + console.log(' Discount Percentage:', discount.toFixed(2)); + + const discountDetails = await generated.DiscountPercentage.valueWithDetails(client, {}); + if (discountDetails.errorCode) { + throw new Error('Failed to get discount'); + } + + // String flag + const greetingMessage = await generated.GreetingMessage.value(client, {}); + console.log(' greetingMessage:', greetingMessage); + + const greetingDetails = await generated.GreetingMessage.valueWithDetails(client, {}); + if (greetingDetails.errorCode) { + throw new Error('Error evaluating string flag'); + } + + // Integer flag + const usernameMaxLength = await generated.UsernameMaxLength.value(client, {}); + console.log(' usernameMaxLength:', usernameMaxLength); + + const usernameDetails = await generated.UsernameMaxLength.valueWithDetails(client, {}); + if (usernameDetails.errorCode) { + throw new Error('Error evaluating int flag'); + } + + // Object flag + const themeCustomization = await generated.ThemeCustomization.value(client, {}); + console.log(' themeCustomization:', themeCustomization); + + const themeDetails = await generated.ThemeCustomization.valueWithDetails(client, {}); + if (themeDetails.errorCode) { + throw new Error('Error evaluating object flag'); + } +} + +// Test the getKey() method functionality for all flags +function testFlagKeys(): void { + console.log('\nFlag keys:'); + console.log(' enableFeatureA flag key:', generated.EnableFeatureA.getKey()); + console.log(' discountPercentage flag key:', generated.DiscountPercentage.getKey()); + console.log(' greetingMessage flag key:', generated.GreetingMessage.getKey()); + console.log(' usernameMaxLength flag key:', generated.UsernameMaxLength.getKey()); + console.log(' themeCustomization flag key:', generated.ThemeCustomization.getKey()); +} + +// Print success messages +function printSuccessMessages(): void { + console.log('\nāœ… Generated NestJS code compiles successfully!'); + console.log('āœ… NestJS decorators work correctly!'); + console.log('āœ… GeneratedOpenFeatureModule integrates properly!'); +} + +async function bootstrap() { + const app = await NestFactory.createApplicationContext(AppModule); + + try { + const client = app.get(OPENFEATURE_CLIENT); + const testService = app.get(TestService); + + testNestJSDecorators(testService); + await testDirectFlagEvaluation(client); + testFlagKeys(); + printSuccessMessages(); + + await app.close(); + process.exit(0); + } catch (error) { + console.error('Error:', error); + await app.close(); + process.exit(1); + } +} + +bootstrap(); diff --git a/test/nestjs-integration/tsconfig.json b/test/nestjs-integration/tsconfig.json new file mode 100644 index 0000000..8e53fa0 --- /dev/null +++ b/test/nestjs-integration/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true + } +}