Skip to content
Merged
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
100 changes: 100 additions & 0 deletions go/apps/api/routes/v2_keys_create_key/200_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,103 @@ func TestCreateKeyWithEncryption(t *testing.T) {
require.Equal(t, keyEncryption.KeyID, res.Body.Data.KeyId)
require.Equal(t, keyEncryption.WorkspaceID, h.Resources().UserWorkspace.ID)
}

func TestCreateKeyConcurrentWithSameExternalId(t *testing.T) {
t.Parallel()

h := testutil.NewHarness(t)
ctx := context.Background()

route := &handler.Handler{
Logger: h.Logger,
DB: h.DB,
Keys: h.Keys,
Auditlogs: h.Auditlogs,
Vault: h.Vault,
}

h.Register(route)

// Create API using testutil helper
api := h.CreateApi(seed.CreateApiRequest{
WorkspaceID: h.Resources().UserWorkspace.ID,
})

rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_key")

headers := http.Header{
"Content-Type": {"application/json"},
"Authorization": {fmt.Sprintf("Bearer %s", rootKey)},
}

// Use same externalId for concurrent requests
externalID := "user_concurrent_test"

// Create multiple keys concurrently with the same externalId
// This simulates the race condition where:
// 1. Request A checks if identity exists - doesn't find it
// 2. Request B checks if identity exists - doesn't find it
// 3. Request A tries to insert identity - succeeds
// 4. Request B tries to insert identity - gets duplicate key error
// 5. Request B handles the error by finding the existing identity
numConcurrent := 5
results := make(chan testutil.TestResponse[handler.Response], numConcurrent)
errors := make(chan error, numConcurrent)

for range numConcurrent {
go func() {
req := handler.Request{
ApiId: api.ID,
ExternalId: &externalID,
}
res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req)
if res.Status != 200 {
errors <- fmt.Errorf("unexpected status code: %d", res.Status)
return
}
results <- res
}()
}

// Collect all results
keyIDs := make([]string, 0, numConcurrent)
for i := 0; i < numConcurrent; i++ {
select {
case res := <-results:
require.Equal(t, 200, res.Status)
require.NotNil(t, res.Body)
require.NotEmpty(t, res.Body.Data.KeyId)
keyIDs = append(keyIDs, res.Body.Data.KeyId)
case err := <-errors:
t.Fatal(err)
}
}

// Verify all keys were created
require.Len(t, keyIDs, numConcurrent)

// Verify all keys reference the same identity
var sharedIdentityID string
for i, keyID := range keyIDs {
key, err := db.Query.FindKeyByID(ctx, h.DB.RO(), keyID)
require.NoError(t, err)
require.True(t, key.IdentityID.Valid)

if i == 0 {
sharedIdentityID = key.IdentityID.String
} else {
require.Equal(t, sharedIdentityID, key.IdentityID.String,
"All concurrent keys should reference the same identity")
}
}

// Verify only one identity was created
identity, err := db.Query.FindIdentity(ctx, h.DB.RO(), db.FindIdentityParams{
WorkspaceID: h.Resources().UserWorkspace.ID,
Identity: externalID,
Deleted: false,
})
require.NoError(t, err)
require.Equal(t, sharedIdentityID, identity.ID)
require.Equal(t, externalID, identity.ExternalID)
}
Loading