From 47d0dd3769139e4bd57bb8454533e390f49dac25 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 12 Dec 2024 10:06:34 +0100 Subject: [PATCH 1/5] initial commit --- cli/prompt_secret_test.ts | 388 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 cli/prompt_secret_test.ts diff --git a/cli/prompt_secret_test.ts b/cli/prompt_secret_test.ts new file mode 100644 index 000000000000..1b64def66042 --- /dev/null +++ b/cli/prompt_secret_test.ts @@ -0,0 +1,388 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "@std/assert/equals"; +import { promptSecret } from "./prompt_secret.ts"; +import { restore, stub } from "@std/testing/mock"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +Deno.test("promptSecret() handles CR", () => { + stub(Deno.stdin, "setRaw"); + + const expectedOutput = [ + "Please provide the password: ", + "\n", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + let readIndex = 0; + + const inputs = [ + "\r", + ]; + + stub( + Deno.stdin, + "readSync", + (data: Uint8Array) => { + const input = inputs[readIndex++]; + const bytes = encoder.encode(input); + data.set(bytes); + return bytes.length; + }, + ); + + const password = promptSecret("Please provide the password:"); + assertEquals(password, ""); + assertEquals(expectedOutput, actualOutput); + restore(); +}); + +Deno.test("promptSecret() handles LF", () => { + stub(Deno.stdin, "setRaw"); + + const expectedOutput = [ + "Please provide the password: ", + "\n", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + let readIndex = 0; + + const inputs = [ + "\n", + ]; + + stub( + Deno.stdin, + "readSync", + (data: Uint8Array) => { + const input = inputs[readIndex++]; + const bytes = encoder.encode(input); + data.set(bytes); + return bytes.length; + }, + ); + + const password = promptSecret("Please provide the password:"); + assertEquals(password, ""); + assertEquals(expectedOutput, actualOutput); + restore(); +}); + +Deno.test("promptSecret() handles input", () => { + stub(Deno.stdin, "setRaw"); + + const expectedOutput = [ + "Please provide the password: ", + "\r\x1b[K", + "Please provide the password: *", + "\r\x1b[K", + "Please provide the password: **", + "\r\x1b[K", + "Please provide the password: ***", + "\r\x1b[K", + "Please provide the password: ****", + "\n", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + let readIndex = 0; + + const inputs = [ + "d", + "e", + "n", + "o", + "\r", + ]; + + stub( + Deno.stdin, + "readSync", + (data: Uint8Array) => { + const input = inputs[readIndex++]; + const bytes = encoder.encode(input); + data.set(bytes); + return bytes.length; + }, + ); + + const password = promptSecret("Please provide the password:"); + + assertEquals(password, "deno"); + assertEquals(expectedOutput, actualOutput); + restore(); +}); + +Deno.test("promptSecret() handles DEL", () => { + stub(Deno.stdin, "setRaw"); + + const expectedOutput = [ + "Please provide the password: ", + "\r\x1b[K", + "Please provide the password: *", + "\r\x1b[K", + "Please provide the password: ", + "\r\x1b[K", + "Please provide the password: *", + "\r\x1b[K", + "Please provide the password: **", + "\r\x1b[K", + "Please provide the password: ***", + "\r\x1b[K", + "Please provide the password: ****", + "\n", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + let readIndex = 0; + + const inputs = [ + "n", + "\x7f", + "d", + "e", + "n", + "o", + "\r", + ]; + + stub( + Deno.stdin, + "readSync", + (data: Uint8Array) => { + const input = inputs[readIndex++]; + const bytes = encoder.encode(input); + data.set(bytes); + return bytes.length; + }, + ); + + const password = promptSecret("Please provide the password:"); + + assertEquals(password, "deno"); + assertEquals(expectedOutput, actualOutput); + restore(); +}); + +Deno.test("promptSecret() handles BS", () => { + stub(Deno.stdin, "setRaw"); + + const expectedOutput = [ + "Please provide the password: ", + "\r\x1b[K", + "Please provide the password: *", + "\r\x1b[K", + "Please provide the password: ", + "\r\x1b[K", + "Please provide the password: *", + "\r\x1b[K", + "Please provide the password: **", + "\r\x1b[K", + "Please provide the password: ***", + "\r\x1b[K", + "Please provide the password: ****", + "\n", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + let readIndex = 0; + + const inputs = [ + "n", + "\b", + "d", + "e", + "n", + "o", + "\r", + ]; + + stub( + Deno.stdin, + "readSync", + (data: Uint8Array) => { + const input = inputs[readIndex++]; + const bytes = encoder.encode(input); + data.set(bytes); + return bytes.length; + }, + ); + + const password = promptSecret("Please provide the password:"); + + assertEquals(password, "deno"); + assertEquals(expectedOutput, actualOutput); + restore(); +}); + +Deno.test("promptSecret() handles clear option", () => { + stub(Deno.stdin, "setRaw"); + + const expectedOutput = [ + "Please provide the password: ", + "\r\x1b[K", + "Please provide the password: *", + "\r\x1b[K", + "Please provide the password: **", + "\r\x1b[K", + "Please provide the password: ***", + "\r\x1b[K", + "Please provide the password: ****", + "\r\x1b[K", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + let readIndex = 0; + + const inputs = [ + "d", + "e", + "n", + "o", + "\r", + ]; + + stub( + Deno.stdin, + "readSync", + (data: Uint8Array) => { + const input = inputs[readIndex++]; + const bytes = encoder.encode(input); + data.set(bytes); + return bytes.length; + }, + ); + + const password = promptSecret("Please provide the password:", { + clear: true, + }); + + assertEquals(password, "deno"); + assertEquals(expectedOutput, actualOutput); + restore(); +}); + +Deno.test("promptSecret() handles mask option", () => { + stub(Deno.stdin, "setRaw"); + + const expectedOutput = [ + "Please provide the password: ", + "\r\x1b[K", + "Please provide the password: $", + "\r\x1b[K", + "Please provide the password: $$", + "\r\x1b[K", + "Please provide the password: $$$", + "\r\x1b[K", + "Please provide the password: $$$$", + "\n", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + let readIndex = 0; + + const inputs = [ + "d", + "e", + "n", + "o", + "\r", + ]; + + stub( + Deno.stdin, + "readSync", + (data: Uint8Array) => { + const input = inputs[readIndex++]; + const bytes = encoder.encode(input); + data.set(bytes); + return bytes.length; + }, + ); + + const password = promptSecret("Please provide the password:", { mask: "$" }); + + assertEquals(password, "deno"); + assertEquals(expectedOutput, actualOutput); + restore(); +}); From a8326ca2d0a9ae65d8d58a0407e2717f43921d5d Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 12 Dec 2024 10:15:21 +0100 Subject: [PATCH 2/5] update --- cli/prompt_secret.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/prompt_secret.ts b/cli/prompt_secret.ts index 12e46d02ea64..05828f22dddd 100644 --- a/cli/prompt_secret.ts +++ b/cli/prompt_secret.ts @@ -48,10 +48,6 @@ export function promptSecret( ): string | null { const { mask = "*", clear } = options ?? {}; - if (!input.isTerminal()) { - return null; - } - // Make the output consistent with the built-in prompt() message += " "; const callback = !mask ? undefined : (n: number) => { From fdbe7c188abe29ad967e1b9f56b0dcdc583bacfc Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 13 Dec 2024 22:38:53 +0100 Subject: [PATCH 3/5] stub Deno.stdin.isTerminal() --- cli/prompt_secret.ts | 4 ++++ cli/prompt_secret_test.ts | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/cli/prompt_secret.ts b/cli/prompt_secret.ts index 05828f22dddd..12e46d02ea64 100644 --- a/cli/prompt_secret.ts +++ b/cli/prompt_secret.ts @@ -48,6 +48,10 @@ export function promptSecret( ): string | null { const { mask = "*", clear } = options ?? {}; + if (!input.isTerminal()) { + return null; + } + // Make the output consistent with the built-in prompt() message += " "; const callback = !mask ? undefined : (n: number) => { diff --git a/cli/prompt_secret_test.ts b/cli/prompt_secret_test.ts index 1b64def66042..d56af7e90ed5 100644 --- a/cli/prompt_secret_test.ts +++ b/cli/prompt_secret_test.ts @@ -9,6 +9,7 @@ const decoder = new TextDecoder(); Deno.test("promptSecret() handles CR", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "Please provide the password: ", @@ -52,6 +53,7 @@ Deno.test("promptSecret() handles CR", () => { Deno.test("promptSecret() handles LF", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "Please provide the password: ", @@ -95,6 +97,7 @@ Deno.test("promptSecret() handles LF", () => { Deno.test("promptSecret() handles input", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "Please provide the password: ", @@ -151,6 +154,7 @@ Deno.test("promptSecret() handles input", () => { Deno.test("promptSecret() handles DEL", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "Please provide the password: ", @@ -213,6 +217,7 @@ Deno.test("promptSecret() handles DEL", () => { Deno.test("promptSecret() handles BS", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "Please provide the password: ", @@ -275,6 +280,7 @@ Deno.test("promptSecret() handles BS", () => { Deno.test("promptSecret() handles clear option", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "Please provide the password: ", @@ -333,6 +339,7 @@ Deno.test("promptSecret() handles clear option", () => { Deno.test("promptSecret() handles mask option", () => { stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); const expectedOutput = [ "Please provide the password: ", From 168bf8356b5b09ce78044d144a7a012d11d75ea0 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 13 Dec 2024 22:44:11 +0100 Subject: [PATCH 4/5] add isTerminal() test --- cli/prompt_secret_test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cli/prompt_secret_test.ts b/cli/prompt_secret_test.ts index d56af7e90ed5..a0b67a721e18 100644 --- a/cli/prompt_secret_test.ts +++ b/cli/prompt_secret_test.ts @@ -393,3 +393,27 @@ Deno.test("promptSecret() handles mask option", () => { assertEquals(expectedOutput, actualOutput); restore(); }); + +Deno.test("promptSecret() returns null if Deno.stdin.isTerminal() is false", () => { + stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => false); + + const expectedOutput: string[] = []; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + const password = promptSecret("Please provide the password:"); + assertEquals(password, null); + assertEquals(expectedOutput, actualOutput); + restore(); +}); From fd502893a13b3b31dc8ce3c73b7858eca7f4f4bf Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 14 Dec 2024 01:00:10 +0100 Subject: [PATCH 5/5] add empty readSync tests --- cli/prompt_secret_test.ts | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/cli/prompt_secret_test.ts b/cli/prompt_secret_test.ts index a0b67a721e18..ac2d257f1abf 100644 --- a/cli/prompt_secret_test.ts +++ b/cli/prompt_secret_test.ts @@ -417,3 +417,63 @@ Deno.test("promptSecret() returns null if Deno.stdin.isTerminal() is false", () assertEquals(expectedOutput, actualOutput); restore(); }); + +Deno.test("promptSecret() handles null readSync", () => { + stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); + + const expectedOutput = [ + "Please provide the password: ", + "\n", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + stub(Deno.stdin, "readSync", () => null); + + const password = promptSecret("Please provide the password:"); + + assertEquals(password, ""); + assertEquals(expectedOutput, actualOutput); + restore(); +}); + +Deno.test("promptSecret() handles empty readSync", () => { + stub(Deno.stdin, "setRaw"); + stub(Deno.stdin, "isTerminal", () => true); + + const expectedOutput = [ + "Please provide the password: ", + "\n", + ]; + + const actualOutput: string[] = []; + + stub( + Deno.stdout, + "writeSync", + (data: Uint8Array) => { + const output = decoder.decode(data); + actualOutput.push(output); + return data.length; + }, + ); + + stub(Deno.stdin, "readSync", () => 0); + + const password = promptSecret("Please provide the password:"); + + assertEquals(password, ""); + assertEquals(expectedOutput, actualOutput); + restore(); +});