Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
22 changes: 13 additions & 9 deletions pkg/bsql/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
)

type staticValidator interface {
StaticValidate(ctx context.Context, s *SQLSyncer) error
}

// Config represents the overall connector configuration.
type Config struct {
// AppName is the application name that identifies the connector.
Expand Down Expand Up @@ -42,27 +46,27 @@ type DatabaseConfig struct {
// DSN is the Database Source Name connection string (optional if using structured fields).
// Supports environment variable expansion via ${VAR_NAME} syntax.
// Example: "postgres://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?sslmode=disable"
DSN string `yaml:"dsn" json:"dsn"`
DSN string `yaml:"dsn" json:"dsn" validate:"required"`
Copy link
Contributor

Choose a reason for hiding this comment

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

DSN is required only if the other fields aren't, and vice-versa.


// Structured connection fields (optional, override DSN components when set)

// Scheme is the database type (e.g., "postgres", "mysql", "sqlserver", "oracle", "hdb")
Scheme string `yaml:"scheme" json:"scheme"`
Scheme string `yaml:"scheme" json:"scheme" validate:"required"`

// Host is the database server hostname or IP address (may include port for some databases)
Host string `yaml:"host" json:"host"`
Host string `yaml:"host" json:"host" validate:"required"`

// Port is the database server port number
Port string `yaml:"port" json:"port"`
Port string `yaml:"port" json:"port" validate:"required"`

// Database is the name of the database to connect to
Database string `yaml:"database" json:"database"`
Database string `yaml:"database" json:"database" validate:"required"`

// User is the database username used for authentication
User string `yaml:"user" json:"user"`
User string `yaml:"user" json:"user" validate:"required"`

// Password is the database password used for authentication
Password string `yaml:"password" json:"password"`
Password string `yaml:"password" json:"password" validate:"required"`

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validation tags conflict with documented behavior.

All DatabaseConfig fields are marked validate:"required", but the documentation (lines 42-48) explicitly states:

  • DSN is "optional if using structured fields"
  • Structured fields are "optional, override DSN components when set"

Making all fields required prevents users from using either DSN-only or structured-field-only configurations as documented.

Consider one of these approaches:

  1. Remove the validate:"required" tags and implement custom validation logic that checks for either DSN or complete structured fields
  2. Update the documentation to reflect that all fields are actually required
  3. Use conditional validation to require either DSN OR all structured fields

Example custom validation approach:

func (d *DatabaseConfig) Validate() error {
    if d.DSN == "" && (d.Scheme == "" || d.Host == "" || d.Port == "" || d.Database == "" || d.User == "" || d.Password == "") {
        return fmt.Errorf("either DSN or all structured fields (scheme, host, port, database, user, password) must be provided")
    }
    return nil
}
🤖 Prompt for AI Agents
In pkg/bsql/config.go around lines 49 to 70, the struct fields are all marked
`validate:"required"` which conflicts with the documented behavior that DSN or
the structured fields may be used independently; remove the
`validate:"required"` tags from these fields and implement a `Validate() error`
method on DatabaseConfig that enforces the rule "either DSN is provided or all
structured fields (scheme, host, port, database, user, password) are present"
(return a clear error message otherwise), and ensure callers that load this
config invoke that Validate() method after unmarshalling.

// Params contains additional connection parameters (e.g., {"sslmode": "disable", "timeout": "30s"})
Params map[string]string `yaml:"params" json:"params"`
Expand Down Expand Up @@ -105,7 +109,7 @@ type ListQuery struct {
Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"`

// Query is the SQL statement used to fetch a list of resources.
Query string `yaml:"query" json:"query"`
Query string `yaml:"query" json:"query" validate:"required"`

// Pagination defines the pagination strategy and settings for the list query.
Pagination *Pagination `yaml:"pagination" json:"pagination"`
Expand Down Expand Up @@ -241,7 +245,7 @@ type EntitlementsQuery struct {
Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"`

// Query is the SQL statement used to fetch dynamic entitlements.
Query string `yaml:"query" json:"query"`
Query string `yaml:"query" json:"query" validate:"required"`
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

[nitpick] Added validate:"required" struct tags appear to be unused. These tags are typically used with validation libraries like go-playground/validator, but there's no evidence of such a library being used in the codebase to process these tags. The actual validation is done through the StaticValidate methods, not through struct tag validation.

If these tags are not intended to be used by a validation library, they should be removed to avoid confusion. If they are intended for future use, that should be documented.

Suggested change
Query string `yaml:"query" json:"query" validate:"required"`
Query string `yaml:"query" json:"query"`

Copilot uses AI. Check for mistakes.

// Pagination defines how pagination should be handled for the entitlements query.
Pagination *Pagination `yaml:"pagination" json:"pagination"`
Expand Down
14 changes: 14 additions & 0 deletions pkg/bsql/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ func parseToken(token string) (*queryTokenOpts, error) {
return opts, nil
}

func (s *SQLSyncer) queryVars(query string) ([]string, error) {
result := make([]string, 0)

for _, token := range queryOptRegex.FindAllString(query, -1) {
opts, err := parseToken(token)
if err != nil {
return nil, err
}
result = append(result, opts.Key)
}

return result, nil
}

func (s *SQLSyncer) parseQueryOpts(pCtx *paginationContext, query string, vars map[string]any) (string, []interface{}, bool, error) {
if vars == nil {
vars = make(map[string]any)
Expand Down
69 changes: 69 additions & 0 deletions pkg/bsql/sql_syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bsql
import (
"context"
"database/sql"
"fmt"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/connectorbuilder"
Expand Down Expand Up @@ -68,3 +69,71 @@ func NewActionSyncer(ctx context.Context, db *sql.DB, dbEngine database.DbEngine
fullConfig: fullConfig,
}, nil
}

func (s *SQLSyncer) validateInternal(ctx context.Context, anyV any) error {
if anyV == nil {
return nil
}

if v, ok := anyV.(staticValidator); ok {
err := v.StaticValidate(ctx, s)
if err != nil {
return err
}
}

return nil
}

func (s *SQLSyncer) validateFormatErr(field string, err error) error {
rsTypeId := s.resourceType.Id

return fmt.Errorf("validation error for resource type %q, field %q: %w", rsTypeId, field, err)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

This method has a potential nil pointer dereference bug. When validating actions (lines 95-101), if an error occurs, validateFormatErr is called which accesses s.resourceType.Id. However, NewActionSyncer (line 63-71) creates a syncer with resourceType: nil, which will cause a panic when validating actions with errors.

You should check if resourceType is nil before accessing it:

func (s *SQLSyncer) validateFormatErr(field string, err error) error {
	if s.resourceType != nil {
		return fmt.Errorf("validation error for resource type %q, field %q: %w", s.resourceType.Id, field, err)
	}
	return fmt.Errorf("validation error for field %q: %w", field, err)
}
Suggested change
rsTypeId := s.resourceType.Id
return fmt.Errorf("validation error for resource type %q, field %q: %w", rsTypeId, field, err)
if s.resourceType != nil {
return fmt.Errorf("validation error for resource type %q, field %q: %w", s.resourceType.Id, field, err)
}
return fmt.Errorf("validation error for field %q: %w", field, err)

Copilot uses AI. Check for mistakes.
}

func (s *SQLSyncer) Validate(ctx context.Context) error {
if s.fullConfig.Actions != nil {
for key, action := range s.fullConfig.Actions {
err := s.validateInternal(ctx, &action)
if err != nil {
return s.validateFormatErr(fmt.Sprintf("Action[%s]", key), err)
}
}
}

if err := s.validateInternal(ctx, s.config.List); err != nil {
return s.validateFormatErr("list", err)
}

if s.config.Entitlements != nil {
if err := s.validateInternal(ctx, s.config.Entitlements); err != nil {
return s.validateFormatErr("entitlements", err)
}
}

if s.config.StaticEntitlements != nil {
if err := s.validateInternal(ctx, s.config.StaticEntitlements); err != nil {
return s.validateFormatErr("static_entitlements", err)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The validation logic for StaticEntitlements is incorrect. s.config.StaticEntitlements is of type []*EntitlementMapping (a slice), but validateInternal expects a single item that implements staticValidator. The slice itself doesn't implement this interface, only individual *EntitlementMapping items do.

This code will silently skip validation instead of validating each entitlement mapping. You should iterate through the slice and validate each element:

if s.config.StaticEntitlements != nil {
	for i, entitlement := range s.config.StaticEntitlements {
		if err := s.validateInternal(ctx, entitlement); err != nil {
			return s.validateFormatErr(fmt.Sprintf("static_entitlements[%d]", i), err)
		}
	}
}
Suggested change
if err := s.validateInternal(ctx, s.config.StaticEntitlements); err != nil {
return s.validateFormatErr("static_entitlements", err)
for i, entitlement := range s.config.StaticEntitlements {
if err := s.validateInternal(ctx, entitlement); err != nil {
return s.validateFormatErr(fmt.Sprintf("static_entitlements[%d]", i), err)
}

Copilot uses AI. Check for mistakes.
}
}

if s.config.Grants != nil {
if err := s.validateInternal(ctx, s.config.Grants); err != nil {
return s.validateFormatErr("grants", err)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The validation logic for Grants is incorrect. s.config.Grants is of type []*GrantsQuery (a slice), but validateInternal expects a single item that implements staticValidator. The slice itself doesn't implement this interface, only individual *GrantsQuery items do.

This code will silently skip validation instead of validating each grant query. You should iterate through the slice and validate each element:

if s.config.Grants != nil {
	for i, grant := range s.config.Grants {
		if err := s.validateInternal(ctx, grant); err != nil {
			return s.validateFormatErr(fmt.Sprintf("grants[%d]", i), err)
		}
	}
}
Suggested change
if err := s.validateInternal(ctx, s.config.Grants); err != nil {
return s.validateFormatErr("grants", err)
for i, grant := range s.config.Grants {
if err := s.validateInternal(ctx, grant); err != nil {
return s.validateFormatErr(fmt.Sprintf("grants[%d]", i), err)
}

Copilot uses AI. Check for mistakes.
}
}

if s.config.AccountProvisioning != nil {
if err := s.validateInternal(ctx, s.config.AccountProvisioning); err != nil {
return s.validateFormatErr("account_provisioning", err)
}
}

if s.config.CredentialRotation != nil {
if err := s.validateInternal(ctx, s.config.CredentialRotation); err != nil {
return s.validateFormatErr("credential_rotation", err)
}
}

return nil
}
146 changes: 146 additions & 0 deletions pkg/bsql/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package bsql

import (
"context"
"errors"
"fmt"
)

func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) error {
if query == "" {
return fmt.Errorf("list query is required")
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The error message "list query is required" is misleading. This function validateVarsInQuery is used for various types of queries (list, entitlements, grants, provisioning, etc.), not just list queries. The error message should be more generic.

Consider changing to:

return fmt.Errorf("query is required")
Suggested change
return fmt.Errorf("list query is required")
return fmt.Errorf("query is required")

Copilot uses AI. Check for mistakes.
}

usedVars, err := s.queryVars(query)
if err != nil {
return fmt.Errorf("failed to parse list query for variables: %w", err)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The error message "failed to parse list query for variables" is misleading. This function is reused for various types of queries (list, entitlements, grants, provisioning, etc.), not just list queries. The error message should be more generic.

Consider changing to:

return fmt.Errorf("failed to parse query for variables: %w", err)
Suggested change
return fmt.Errorf("failed to parse list query for variables: %w", err)
return fmt.Errorf("failed to parse query for variables: %w", err)

Copilot uses AI. Check for mistakes.
}

if vars == nil {
vars = make(map[string]string)
}

for _, v := range usedVars {
if _, ok := vars[v]; !ok {
if v == "limit" || v == "offset" || v == "cursor" {
continue
}
return fmt.Errorf("list query uses variable '%s' which is not defined in vars", v)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The error message "list query uses variable" is misleading. This function is reused for various types of queries (list, entitlements, grants, provisioning, etc.), not just list queries. The error message should be more generic.

Consider changing to:

return fmt.Errorf("query uses variable '%s' which is not defined in vars", v)
Suggested change
return fmt.Errorf("list query uses variable '%s' which is not defined in vars", v)
return fmt.Errorf("query uses variable '%s' which is not defined in vars", v)

Copilot uses AI. Check for mistakes.
}
}

return nil
}
Comment on lines 9 to 33
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error messages are too specific.

The validateVarsInQuery function is used for validating all query types (list, entitlements, grants, provisioning, etc.), but the error messages specifically mention "list query" (lines 10, 15, and 27), which is misleading when validating other query types.

Make the error messages generic:

 func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) error {
 	if query == "" {
-		return fmt.Errorf("list query is required")
+		return fmt.Errorf("query is required")
 	}
 
 	usedVars, err := s.queryVars(query)
 	if err != nil {
-		return fmt.Errorf("failed to parse list query for variables: %w", err)
+		return fmt.Errorf("failed to parse query for variables: %w", err)
 	}
 
 	if vars == nil {
 		vars = make(map[string]string)
 	}
 
 	for _, v := range usedVars {
 		if _, ok := vars[v]; !ok {
 			if v == "limit" || v == "offset" || v == "cursor" {
 				continue
 			}
-			return fmt.Errorf("list query uses variable '%s' which is not defined in vars", v)
+			return fmt.Errorf("query uses variable '%s' which is not defined in vars", v)
 		}
 	}
 
 	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) error {
if query == "" {
return fmt.Errorf("list query is required")
}
usedVars, err := s.queryVars(query)
if err != nil {
return fmt.Errorf("failed to parse list query for variables: %w", err)
}
if vars == nil {
vars = make(map[string]string)
}
for _, v := range usedVars {
if _, ok := vars[v]; !ok {
if v == "limit" || v == "offset" || v == "cursor" {
continue
}
return fmt.Errorf("list query uses variable '%s' which is not defined in vars", v)
}
}
return nil
}
func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) error {
if query == "" {
return fmt.Errorf("query is required")
}
usedVars, err := s.queryVars(query)
if err != nil {
return fmt.Errorf("failed to parse query for variables: %w", err)
}
if vars == nil {
vars = make(map[string]string)
}
for _, v := range usedVars {
if _, ok := vars[v]; !ok {
if v == "limit" || v == "offset" || v == "cursor" {
continue
}
return fmt.Errorf("query uses variable '%s' which is not defined in vars", v)
}
}
return nil
}
🤖 Prompt for AI Agents
In pkg/bsql/validate.go around lines 8 to 32, the error messages reference "list
query" but this function validates any query type; change the three messages to
be generic (e.g., "query is required", "failed to parse query for variables:
%w", and "query uses variable '%s' which is not defined in vars") so they no
longer mention "list"; keep the same formatting and error wrapping semantics and
do not alter the validation logic.


func (l *ListQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error {
return validateVarsInQuery(s, l.Query, l.Vars)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add godoc comments for exported methods.

All seven exported StaticValidate methods lack documentation. Each should have a godoc comment describing what it validates.

As per coding guidelines, comments for exported items must be complete sentences ending with periods.

Example for ListQuery.StaticValidate:

+// StaticValidate verifies that all variables used in the query are defined in Vars.
 func (l *ListQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error {
 	return validateVarsInQuery(s, l.Query, l.Vars)
 }

Apply similar documentation to the remaining methods:

  • EntitlementsQuery.StaticValidate (line 39)
  • EntitlementMapping.StaticValidate (line 43)
  • GrantsQuery.StaticValidate (line 69)
  • AccountProvisioning.StaticValidate (line 73)
  • CredentialRotation.StaticValidate (line 122)
  • ActionConfig.StaticValidate (line 135)

Also applies to: 39-41, 43-67, 69-71, 73-120, 122-133, 135-146

🤖 Prompt for AI Agents
In pkg/bsql/validate.go around lines 35-37, 39-41, 43-67, 69-71, 73-120,
122-133, and 135-146, each exported StaticValidate method is missing godoc; add
a one-sentence godoc comment immediately above each method (e.g.,
"ListQuery.StaticValidate validates that the query and provided variables are
consistent and returns an error if not.") that describes what the method
validates and ends with a period; ensure the comment names the receiver/type and
uses a full sentence for each of: ListQuery.StaticValidate,
EntitlementsQuery.StaticValidate, EntitlementMapping.StaticValidate,
GrantsQuery.StaticValidate, AccountProvisioning.StaticValidate,
CredentialRotation.StaticValidate, and ActionConfig.StaticValidate.


func (l *EntitlementsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error {
return validateVarsInQuery(s, l.Query, l.Vars)
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

Missing validation of EntitlementsQuery.Map field. The EntitlementsQuery.StaticValidate method only validates the query variables but doesn't validate the Map field, which is a slice of EntitlementMapping objects. Each EntitlementMapping has provisioning queries that should also be validated.

Consider adding validation for the Map field:

func (l *EntitlementsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error {
	if err := validateVarsInQuery(s, l.Query, l.Vars); err != nil {
		return err
	}
	
	for i, mapping := range l.Map {
		if mapping != nil {
			if err := mapping.StaticValidate(ctx, s); err != nil {
				return fmt.Errorf("map[%d]: %w", i, err)
			}
		}
	}
	
	return nil
}
Suggested change
return validateVarsInQuery(s, l.Query, l.Vars)
if err := validateVarsInQuery(s, l.Query, l.Vars); err != nil {
return err
}
for i, mapping := range l.Map {
if mapping != nil {
if err := mapping.StaticValidate(ctx, s); err != nil {
return fmt.Errorf("map[%d]: %w", i, err)
}
}
}
return nil

Copilot uses AI. Check for mistakes.
}

func (l *EntitlementMapping) StaticValidate(ctx context.Context, s *SQLSyncer) error {
if l.Provisioning == nil {
return nil
}

if l.Provisioning.Grant != nil {
for _, query := range l.Provisioning.Grant.Queries {
err := validateVarsInQuery(s, query, l.Provisioning.Vars)
if err != nil {
return err
}
}
}

if l.Provisioning.Revoke != nil {
for _, query := range l.Provisioning.Revoke.Queries {
err := validateVarsInQuery(s, query, l.Provisioning.Vars)
if err != nil {
return err
}
}
}

return nil
}

func (l *GrantsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error {
return validateVarsInQuery(s, l.Query, l.Vars)
}

func (l *AccountProvisioning) StaticValidate(ctx context.Context, s *SQLSyncer) error {

Check failure on line 73 in pkg/bsql/validate.go

View workflow job for this annotation

GitHub Actions / go-lint

unnecessary leading newline (whitespace)

if l.Credentials == nil {
return errors.New("no credentials defined")
}

if l.Credentials.EncryptedPassword == nil &&
l.Credentials.RandomPassword == nil &&
l.Credentials.NoPassword == nil {
return errors.New("no credential method defined")
}

if l.Credentials.RandomPassword != nil {
if l.Credentials.RandomPassword.MaxLength <= 0 {
return errors.New("random password max_length must be greater than zero")
}

if l.Credentials.RandomPassword.MinLength <= 0 {
return errors.New("random password min_length must be greater than zero")
}

if l.Credentials.RandomPassword.MinLength > l.Credentials.RandomPassword.MaxLength {
return errors.New("random password min_length cannot be greater than max_length")
}
}

if l.Create == nil {
return errors.New("no create functions defined")
}

for _, query := range l.Create.Queries {
err := validateVarsInQuery(s, query, l.Create.Vars)
if err != nil {
return err
}
}

if l.Validate == nil {
return errors.New("no validate functions defined")
}

err := validateVarsInQuery(s, l.Validate.Query, l.Validate.Vars)
if err != nil {
return err
}

return nil
}

func (l *CredentialRotation) StaticValidate(ctx context.Context, s *SQLSyncer) error {
if l.Update != nil {
for _, query := range l.Update.Queries {
err := validateVarsInQuery(s, query, l.Update.Vars)
if err != nil {
return err
}
}
}

return nil
}

func (l *ActionConfig) StaticValidate(ctx context.Context, s *SQLSyncer) error {
availableVars := make(map[string]string)
for k, v := range l.Vars {
availableVars[k] = v
}

for k, config := range l.Arguments {
availableVars[k] = config.Name
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

[nitpick] Semantically incorrect value assignment in ActionConfig.StaticValidate. At line 142, the code assigns config.Name (the argument's display name) as the value in availableVars, but should use config.Type for consistency with how Vars maps are defined elsewhere (e.g., ListQuery.Vars at line 109 of config.go is map[string]string where values are types).

While this doesn't affect functionality (since validateVarsInQuery only checks key existence), it's semantically incorrect:

for k, config := range l.Arguments {
    availableVars[k] = config.Type  // Use Type instead of Name
}
Suggested change
availableVars[k] = config.Name
availableVars[k] = config.Type

Copilot uses AI. Check for mistakes.
}

return validateVarsInQuery(s, l.Query, availableVars)
}
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

Missing test coverage for newly added validation methods. The test file only covers ListQuery validation, but the new validation logic includes several other important validators:

  • EntitlementsQuery.StaticValidate
  • EntitlementMapping.StaticValidate
  • GrantsQuery.StaticValidate
  • AccountProvisioning.StaticValidate (with complex credential and password length validation)
  • CredentialRotation.StaticValidate
  • ActionConfig.StaticValidate

Consider adding test cases for these validators to ensure they properly catch configuration errors, especially the AccountProvisioning validator which has multiple validation rules.

Copilot uses AI. Check for mistakes.
51 changes: 51 additions & 0 deletions pkg/bsql/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package bsql

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestValidate(t *testing.T) {
tcases := []struct {
name string
validator staticValidator
expectErr bool
}{
{
name: "valid list query",
validator: &ListQuery{
Query: "SELECT * FROM users WHERE id = ?<userid> LIMIT ?<Limit> OFFSET ?<Offset>",
Vars: map[string]string{
"userid": "string",
},
},
expectErr: false,
},
{
name: "invalid list query",
validator: &ListQuery{
Query: "SELECT * FROM users WHERE id = ?<unknown> LIMIT ?<Limit> OFFSET ?<Offset>",
Vars: map[string]string{
"userid": "string",
},
},
expectErr: true,
},
}

for _, tc := range tcases {
t.Run(tc.name, func(t *testing.T) {
ctx := t.Context()

syncer := &SQLSyncer{}

err := tc.validator.StaticValidate(ctx, syncer)
if tc.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
Comment on lines 9 to 51
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

Test coverage is incomplete for the validation functionality. The current test only covers ListQuery validation with two scenarios. Consider adding test cases for:

  1. EntitlementsQuery.StaticValidate
  2. EntitlementMapping.StaticValidate (with Grant/Revoke provisioning)
  3. GrantsQuery.StaticValidate
  4. AccountProvisioning.StaticValidate (Create and Validate queries)
  5. CredentialRotation.StaticValidate (Update queries)
  6. ActionConfig.StaticValidate (with Vars and Arguments)
  7. Edge cases like empty queries, nil vars, and valid reserved variables (limit, offset, cursor)

Copilot uses AI. Check for mistakes.
18 changes: 17 additions & 1 deletion pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,23 @@ func (c *Connector) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error)
// Validate is called to ensure that the connector is properly configured. It should exercise any API credentials
// to be sure that they are valid.
func (c *Connector) Validate(ctx context.Context) (annotations.Annotations, error) {
err := c.db.PingContext(ctx)
syncers, err := c.config.GetSQLSyncers(ctx, c.db, c.dbEngine, c.celEnv)
if err != nil {
return nil, err
}

for _, syncer := range syncers {
if v, ok := syncer.(interface {
Validate(ctx context.Context) error
}); ok {
err := v.Validate(ctx)
if err != nil {
return nil, err
}
}
}

err = c.db.PingContext(ctx)
if err != nil {
return nil, err
}
Expand Down
Loading