Gabe
Gabe

Reputation: 1296

LauchDarkly flag value specific for test case

I'm trying to unit test a function that is partly controlled by a LaunchDarkly flag. My wish is for the flag to be set to true for every test case but one.

To achieve that I'm trying to set specific attributes in the context for the false test case, that can be then evaluated by the LaunchDarkly evaluator, but I can't seem to find the right setup of context keys, context kind, and attributes to make it work.

Here's what I have:

// Test file
import (
    "github.com/launchdarkly/go-server-sdk/v7/ldcomponents"
    "github.com/launchdarkly/go-server-sdk/v7/testhelpers/ldtestdata"
    ld "github.com/launchdarkly/go-server-sdk/v7"
    ofld "github.com/open-feature/go-sdk-contrib/providers/launchdarkly/pkg"
    "github.com/open-feature/go-sdk/openfeature"
)

func NewTestFlaggingClient() (*ldtestdata.TestDataSource, *ld.LDClient) {
    td := ldtestdata.DataSource()

    var ldConfig ld.Config
    ldConfig.ApplicationInfo.ApplicationID = "testing"
    ldConfig.DataSource = td
    ldConfig.Events = ldcomponents.NoEvents()

    ldClient, err := ld.MakeCustomClient("fake-sdk-key", ldConfig, 5*time.Second)
    if err != nil {
        return nil, err
    }

    if err := openfeature.SetProvider(ofld.NewProvider(ldClient)); err != nil {
        return nil, err
    }

    return td, ldClient
}

func Test_myMethod(t *testing.T) {
  // ...
  disabledCtx := openfeature.NewEvaluationContext("user",
    map[string]interface{}{
      "disable-flag": true
    })

  td, flagger := flags.NewTestFlaggingClient()
  
  // Here I'm trying to create a flag that will always be true
  // except when I see the context above
  td.Update(td.Flag("method-enabled").
    BooleanFlag().
    VariationForKey("user", "disable-flag", false).
    FallthroughVariation(true),
  )

    t.Run("Skip if flag is not set", func(t *testing.T) {
      // add the disabledCtx to the parent context
      ctx := openfeature.WithTransactionContext(ctx, disabledCtx)

      err := myMethod(ctx)
      // ...
    })
}
// myMethod implementation
func myMethod(ctx context.Context) error {
  // the Flag evaluation always receives this anonymous context, alongside the parent context
  anonCtx := openfeature.NewEvaluationContext("anonymous", map[string]any{
    "kind":      "user",
    "anonymous": true,
  })

  if !p.clients.Flags.Boolean(ctx, "mm-return-processor-enabled", false, anonCtx) {
    return nil
  }
}

I can't really find a working example anywhere, and I tried debugging the openfeature and laumchdarkly packages to see what's breaking, but I've found no way out yet.

Upvotes: 2

Views: 130

Answers (1)

eik
eik

Reputation: 4618

I'm not exactly sure what you are trying to achieve here, but with

    td.Update(td.Flag("method-enabled").
        VariationForUser("user1", false).
        FallthroughVariation(true),
    )

you create a flag method-enabled that is false for user1 and true for everybody else. VariationForUser is a shorthand for VariationForKey("user", ..., which is what you are using. I'm unsure if you really meant a user disable-flag, I go with user1 here.

Assuming your method under test:

package main

import (
    "context"

    "github.com/open-feature/go-sdk/openfeature"
)

func MyMethod(ctx context.Context) (bool, error) {
    of := openfeature.GetApiInstance()
    client := of.GetClient()

    val, err := client.BooleanValue(ctx, "method-enabled", false, openfeature.EvaluationContext{})
    if err != nil {
        return false, err
    }

    return val, nil
}

This test will pass:

package main

import (
    "context"
    "testing"
    "time"

    ld "github.com/launchdarkly/go-server-sdk/v7"
    "github.com/launchdarkly/go-server-sdk/v7/interfaces"
    "github.com/launchdarkly/go-server-sdk/v7/ldcomponents"
    "github.com/launchdarkly/go-server-sdk/v7/testhelpers/ldtestdata"
    ofld "github.com/open-feature/go-sdk-contrib/providers/launchdarkly/pkg"
    "github.com/open-feature/go-sdk/openfeature"
)

func NewTestFlaggingClient() (*ldtestdata.TestDataSource, error) {
    td := ldtestdata.DataSource()

    ldConfig := ld.Config{
        ApplicationInfo: interfaces.ApplicationInfo{
            ApplicationID: "testing",
        },
        DataSource: td,
        Events:     ldcomponents.NoEvents(),
    }

    ldClient, err := ld.MakeCustomClient("fake-sdk-key", ldConfig, 5*time.Second)
    if err != nil {
        return nil, err
    }

    if err := openfeature.SetProvider(ofld.NewProvider(ldClient)); err != nil {
        return nil, err
    }

    return td, err
}

func TestMyMethod(t *testing.T) {
    td, _ := NewTestFlaggingClient()

    td.Update(td.Flag("method-enabled").
        VariationForUser("user1", false).
        FallthroughVariation(true),
    )

    enabledCtx := openfeature.NewEvaluationContext("user2", nil)

    disabledCtx := openfeature.NewEvaluationContext("user1", nil)

    ctx := context.Background()

    type args struct {
        ctx context.Context
    }
    tests := []struct {
        name    string
        args    args
        want    bool
        wantErr bool
    }{
        {
            name: "enabled",
            args: args{openfeature.WithTransactionContext(ctx, enabledCtx)},
            want: true,
        },
        {
            name: "disabled",
            args: args{openfeature.WithTransactionContext(ctx, disabledCtx)},
            want: false,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := MyMethod(tt.args.ctx)
            if (err != nil) != tt.wantErr {
                t.Errorf("MyMethod() error = %v, wantErr %v", err, tt.wantErr)

                return
            }
            if got != tt.want {
                t.Errorf("MyMethod() = %v, want %v", got, tt.want)
            }
        })
    }
}

When you want to use attributes, use:

...
import  "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
...
    td.Update(td.Flag("method-enabled").
        IfMatch("disable-flag", ldvalue.Bool(true)).ThenReturn(false).
        FallthroughVariation(true),
    )

    enabledCtx := openfeature.NewEvaluationContext("user", nil)

    disabledCtx := openfeature.NewEvaluationContext("user", map[string]interface{}{"disable-flag": true})
...

Also, see this document for some attributes mapped from OpenFeature to LaunchDarkly, like anonymous contexts.


As you can easily verify, with MyMethod defined as:

func MyMethod(ctx context.Context) (bool, error) {
    of := openfeature.GetApiInstance()
    client := of.GetClient()

    anonCtx := openfeature.NewEvaluationContext("anonymous", map[string]any{
        "kind":      "user",
        "anonymous": true,
    })

    val, err := client.BooleanValue(ctx, "method-enabled", false, anonCtx)
    if err != nil {
        return false, err
    }

    return val, nil
}

the tests pass too. With "mm-return-processor-enabled" you won't get what you want, since this flag is not defined anywhere else.

Upvotes: 1

Related Questions