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 @@ -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"
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-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..."
Expand Down
98 changes: 98 additions & 0 deletions test/integration/cmd/nestjs/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 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").
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The Node.js version used here (node:20-alpine) is inconsistent with other Node.js-based integration tests. Both the Angular and NodeJS integration tests use node:22-alpine. For consistency and to ensure tests run with the same Node.js version, this should be updated to use node:22-alpine instead.

Suggested change
From("node:20-alpine").
From("node:22-alpine").

Copilot uses AI. Check for mistakes.
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()
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The method used to get the project directory is inconsistent with other integration tests. All other integration tests (angular, csharp, go, nodejs) use filepath.Abs(os.Getenv("PWD")) instead of os.Getwd(). This should be changed to maintain consistency across integration tests and ensure reliability in different execution environments.

Suggested change
projectDir, err := os.Getwd()
projectDir, err := filepath.Abs(os.Getenv("PWD"))

Copilot uses AI. Check for mistakes.
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)
}
}
56 changes: 20 additions & 36 deletions test/integration/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===")
}
21 changes: 21 additions & 0 deletions test/nestjs-integration/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
184 changes: 184 additions & 0 deletions test/nestjs-integration/src/main.ts
Original file line number Diff line number Diff line change
@@ -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';
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The import path for GeneratedOpenFeatureModule is incorrect. The NestJS generator creates a file named 'openfeature-decorators.ts' which exports GeneratedOpenFeatureModule, but this import is trying to load it from 'openfeature-module'. This should be changed to './generated/openfeature-decorators' to match the actual generated file name.

Suggested change
import { GeneratedOpenFeatureModule } from './generated/openfeature-module';
import { GeneratedOpenFeatureModule } from './generated/openfeature-decorators';

Copilot uses AI. Check for mistakes.

// 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<void> {
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<Client>(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);
}
}
Comment on lines 163 to 182

Choose a reason for hiding this comment

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

medium

The bootstrap function is quite long and handles multiple testing concerns. To improve readability and maintainability, consider breaking it down into smaller, more focused functions. For example, you could have separate functions for testing decorators, direct flag evaluation, and getKey() methods.

async function testDecorators(testService: TestService) {
  // ... logic for testing decorators
}

async function testDirectEvaluation(client: Client) {
  // ... logic for testing direct evaluation
}

function testGetKeyMethods() {
  // ... logic for testing getKey()
}

async function bootstrap() {
  const app = await NestFactory.createApplicationContext(AppModule);

  try {
    const client = app.get<Client>(OPENFEATURE_CLIENT);
    const testService = app.get(TestService);

    testDecorators(testService);
    await testDirectEvaluation(client);
    testGetKeyMethods();

    console.log('\n✅ Generated NestJS code compiles successfully!');
    console.log('✅ NestJS decorators work correctly!');
    console.log('✅ GeneratedOpenFeatureModule integrates properly!');

    await app.close();
    process.exit(0);
  } catch (error) {
    console.error('Error:', error);
    await app.close();
    process.exit(1);
  }
}

This structure makes the test flow clearer and each part easier to understand and modify.

Comment on lines +163 to +182

Choose a reason for hiding this comment

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

medium

The bootstrap function contains duplicated code for closing the application context (await app.close()). This can be refactored using a try...catch...finally block to improve maintainability and ensure the cleanup logic is always executed in a single place, regardless of whether an error occurred.

Suggested change
async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule);
try {
const client = app.get<Client>(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);
}
}
async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule);
let exitCode = 0;
try {
const client = app.get<Client>(OPENFEATURE_CLIENT);
const testService = app.get(TestService);
testNestJSDecorators(testService);
await testDirectFlagEvaluation(client);
testFlagKeys();
printSuccessMessages();
} catch (error) {
console.error('Error:', error);
exitCode = 1;
} finally {
await app.close();
process.exit(exitCode);
}
}


bootstrap();
Loading
Loading