Skip to content

feat(sdp): Add unit tests - #5866

Open
agduan wants to merge 6 commits into
genkit-ai:mainfrom
agduan:sdp/6-tests
Open

feat(sdp): Add unit tests#5866
agduan wants to merge 6 commits into
genkit-ai:mainfrom
agduan:sdp/6-tests

Conversation

@agduan

@agduan agduan commented Jul 30, 2026

Copy link
Copy Markdown

(6/6) Adds unit tests for the Sensitive Data Protection (SDP) middleware.

View diff

Checklist (if applicable):

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Sensitive Data Protection (SDP) middleware plugin for Google Cloud in Genkit, allowing input and output message redaction using Google Cloud DLP. Feedback on the implementation highlights a critical security issue where using the logical OR operator (||) could leak sensitive data if the redacted output is an empty string, recommending the nullish coalescing operator (??) instead. Additionally, the reviewer suggested optimizing client initialization to avoid redundant credential checks, removing a @ts-ignore comment by providing a safe fallback array, and expanding the test suite to cover output redaction.

}

const [response] = await dlp.deidentifyContent(request);
return response.item?.value || text;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-high high

Using the logical OR operator (||) here can lead to a security vulnerability. If the Sensitive Data Protection (SDP) API successfully redacts the content by completely removing it (resulting in an empty string ""), the expression response.item?.value || text will evaluate to the original sensitive text because "" is falsy.

Using the nullish coalescing operator (??) ensures that we only fall back to the original text if the response value is strictly null or undefined.

Suggested change
return response.item?.value || text;
return response.item?.value ?? text;

Comment on lines +91 to +117
async function createDlpClient(
options: SdpOptions
): Promise<v2.DlpServiceClient> {
let dlpModule;
try {
dlpModule = await import('@google-cloud/dlp');
} catch (e) {
throw new Error(
'Please install the @google-cloud/dlp package to use the SDP middleware.'
);
}

let envAuth;
if (!options.projectId || !options.credentials) {
envAuth = await credentialsFromEnvironment();
}

const projectId = options.projectId || envAuth?.projectId;
const credentials = options.credentials || envAuth?.credentials;

return new dlpModule.v2.DlpServiceClient({
credentials: credentials as any as NonNullable<
ConstructorParameters<typeof v2.DlpServiceClient>[0]
>['credentials'],
projectId: projectId,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The createDlpClient helper function can be simplified significantly. Since the caller already resolves the credentials and project ID from the environment, we don't need to duplicate that logic here. Passing projectId and credentials directly as arguments makes the function cleaner and avoids potential duplicate calls to credentialsFromEnvironment().

async function createDlpClient(
  projectId?: string,
  credentials?: any
): Promise<v2.DlpServiceClient> {
  let dlpModule;
  try {
    dlpModule = await import('@google-cloud/dlp');
  } catch (e) {
    throw new Error(
      'Please install the @google-cloud/dlp package to use the SDP middleware.'
    );
  }

  return new dlpModule.v2.DlpServiceClient({
    credentials: credentials as any,
    projectId,
  });
}

Comment on lines +221 to +235
const envAuth = await credentialsFromEnvironment();
const projectId = opts.projectId || envAuth.projectId;
const credentials = opts.credentials || envAuth.credentials;

if (!projectId) {
throw new Error(
'Project ID is required for Sensitive Data Protection. Please set the projectId option or configure it in your environment.'
);
}

const client = await createDlpClient({
...opts,
projectId,
credentials,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

We can optimize the client initialization by only calling credentialsFromEnvironment() if either projectId or credentials is missing from the options. This avoids unnecessary environment/file checks when the user has explicitly configured the middleware with inline options. Additionally, we can pass these resolved values directly to the simplified createDlpClient function.

            const envAuth =
              !opts.projectId || !opts.credentials
                ? await credentialsFromEnvironment()
                : null;
            const projectId = opts.projectId || envAuth?.projectId;
            const credentials = opts.credentials || envAuth?.credentials;

            if (!projectId) {
              throw new Error(
                'Project ID is required for Sensitive Data Protection. Please set the projectId option or configure it in your environment.'
              );
            }

            const client = await createDlpClient(projectId, credentials);

Comment on lines +252 to +254
// @ts-ignore - flatMap creates a flat array of content parts
.flatMap((message) => message.content!)
// ignore multimedia content and content that has been cleaned already

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

We can eliminate the @ts-ignore comment by using message.content || []. This provides a safe fallback array and allows TypeScript to correctly infer the types for flatMap.

Suggested change
// @ts-ignore - flatMap creates a flat array of content parts
.flatMap((message) => message.content!)
// ignore multimedia content and content that has been cleaned already
// extract all message content into a single array
.flatMap((message) => message.content || [])

Comment on lines +215 to +216
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current test suite only covers input redaction (where the prompt contains sensitive data). There is no test verifying that output redaction works correctly when the model itself generates sensitive data. Adding a test case for this scenario ensures full coverage of the middleware's output interception path.

  });

  it('should redact sensitive data generated by the model (output redaction)', async () => {
    ai.defineModel({ name: 'sensitiveModel' }, async () => {
      return {
        message: {
          role: 'model',
          content: [{ text: 'Here is my credit card' }],
        },
      };
    });

    const response = await ai.generate({
      model: 'sensitiveModel',
      prompt: 'Hello',
      use: [sensitiveDataProtection({})],
    });

    expect(response.text).toBe('Here is [REDACTED_BY_MOCK]');
  });
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant