Skip to content

Write-only attribute: missing test coverage for Token.nullValue() and null-resolving Lazy values #343

Description

@sakul-learning

Current behavior

TerraformResource.markWriteOnlyAttribute() at packages/cdktn/src/terraform-resource.ts:308 wraps a value in an IResolvable that, at resolve time, checks whether the resolved value is non-null before registering WRITE_ONLY_ATTRIBUTES usage:

if (resolved != null) {
  resource._registerResolveDiscoveredProviderFeatureUsage(
    ProviderFeature.WRITE_ONLY_ATTRIBUTES,
  );
}

The != null (loose inequality) check correctly returns false for both null and undefined, so registration is suppressed for either.

What is currently tested

The test file packages/cdktn/test/write-only.test.ts covers:

  • Literal null — constructor and setter, with and without prior real values
  • Literal undefined — reset, omitted constructor arg
  • Lazy resolving to undefinedLazy.stringValue({ produce: () => undefined })
  • Real values — literal strings and Lazy resolving to strings

What is missing

Two null-equivalent value forms have no test coverage:

  1. Token.nullValue() — the canonical public API for producing an explicit Terraform null:

    public static nullValue(): IResolvable {
      return Token.asAny(null); // wraps `null` in an `Intrinsic`
    }

    Intrinsic.resolve() returns null, and context.resolve(null) returns null, so the != null check correctly suppresses registration. But there is no test passing Token.nullValue() through markWriteOnlyAttribute. A future refactor that changed the check from != null to !== undefined would silently break this path.

  2. Lazy.anyValue({ produce: () => null }) — a Lazy explicitly resolving to null (distinct from undefined). The existing Lazy test only covers undefined-producing Lazy. A null-producing Lazy renders as explicit null in Terraform (vs. omitted attribute for undefined), so it matters that the write-only guard suppresses validation for it.

How to fix

Add two test cases alongside the existing Lazy tests in write-only.test.ts:

test("Token.nullValue(): resolves to null, attribute renders as explicit null, synth passes", () => {
  const { app, stack } = appWithStack();
  const resource = new TestWriteOnlyResource(stack, "test", {});
  resource.secretKeyWo = Token.nullValue() as unknown as string;
  expect(synthesizedSecretKeyWo(stack)).toBeNull();
  expect(() => app.synth()).not.toThrow();
});

test("Lazy producer resolving to null: attribute renders as explicit null, synth passes", () => {
  const { app, stack } = appWithStack();
  const resource = new TestWriteOnlyResource(stack, "test", {});
  resource.secretKeyWo = Lazy.stringValue({ produce: () => null }) as unknown as string;
  expect(synthesizedSecretKeyWo(stack)).toBeNull();
  expect(() => app.synth()).not.toThrow();
});

Alternatively, widen the test resource setter to any | IResolvable | null to remove the need for casts.

Impact

Low risk, test-only change. No behavior change to production code — the existing != null check already handles these cases correctly. The fix confirms that the canonical null-token path stays covered against future null-check refactors.

Flagged during review of PR #296 by @so0k.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions