@@ -16,13 +16,18 @@ limitations under the License.
1616
1717import { decodeBase64 } from "../../src/base64" ;
1818import {
19- randomLowercaseString ,
20- randomString ,
21- randomUppercaseString ,
19+ secureRandomString ,
2220 secureRandomBase64Url ,
21+ secureRandomStringFrom ,
22+ LOWERCASE ,
23+ UPPERCASE ,
2324} from "../../src/randomstring" ;
2425
2526describe ( "Random strings" , ( ) => {
27+ afterEach ( ( ) => {
28+ jest . restoreAllMocks ( ) ;
29+ } ) ;
30+
2631 it . each ( [ 8 , 16 , 32 ] ) ( "secureRandomBase64 generates %i valid base64 bytes" , ( n : number ) => {
2732 const randb641 = secureRandomBase64Url ( n ) ;
2833 const randb642 = secureRandomBase64Url ( n ) ;
@@ -33,34 +38,77 @@ describe("Random strings", () => {
3338 expect ( decoded ) . toHaveLength ( n ) ;
3439 } ) ;
3540
36- it . each ( [ 8 , 16 , 32 ] ) ( "randomString generates string of %i characters" , ( n : number ) => {
37- const rand1 = randomString ( n ) ;
38- const rand2 = randomString ( n ) ;
41+ it . each ( [ 8 , 16 , 32 ] ) ( "secureRandomString generates string of %i characters" , ( n : number ) => {
42+ const rand1 = secureRandomString ( n ) ;
43+ const rand2 = secureRandomString ( n ) ;
3944
4045 expect ( rand1 ) . not . toEqual ( rand2 ) ;
4146
4247 expect ( rand1 ) . toHaveLength ( n ) ;
4348 } ) ;
4449
45- it . each ( [ 8 , 16 , 32 ] ) ( "randomLowercaseString generates lowercase string of %i characters" , ( n : number ) => {
46- const rand1 = randomLowercaseString ( n ) ;
47- const rand2 = randomLowercaseString ( n ) ;
50+ it . each ( [ 8 , 16 , 32 ] ) (
51+ "secureRandomStringFrom generates lowercase string of %i characters when given lowercase" ,
52+ ( n : number ) => {
53+ const rand1 = secureRandomStringFrom ( n , LOWERCASE ) ;
54+ const rand2 = secureRandomStringFrom ( n , LOWERCASE ) ;
4855
49- expect ( rand1 ) . not . toEqual ( rand2 ) ;
56+ expect ( rand1 ) . not . toEqual ( rand2 ) ;
5057
51- expect ( rand1 ) . toHaveLength ( n ) ;
58+ expect ( rand1 ) . toHaveLength ( n ) ;
59+
60+ expect ( rand1 . toLowerCase ( ) ) . toEqual ( rand1 ) ;
61+ } ,
62+ ) ;
63+
64+ it . each ( [ 8 , 16 , 32 ] ) (
65+ "secureRandomStringFrom generates uppercase string of %i characters when given uppercase" ,
66+ ( n : number ) => {
67+ const rand1 = secureRandomStringFrom ( n , UPPERCASE ) ;
68+ const rand2 = secureRandomStringFrom ( n , UPPERCASE ) ;
69+
70+ expect ( rand1 ) . not . toEqual ( rand2 ) ;
71+
72+ expect ( rand1 ) . toHaveLength ( n ) ;
5273
53- expect ( rand1 . toLowerCase ( ) ) . toEqual ( rand1 ) ;
74+ expect ( rand1 . toUpperCase ( ) ) . toEqual ( rand1 ) ;
75+ } ,
76+ ) ;
77+
78+ it ( "throws if given character set less than 2 characters" , ( ) => {
79+ expect ( ( ) => secureRandomStringFrom ( 8 , "a" ) ) . toThrow ( ) ;
5480 } ) ;
5581
56- it . each ( [ 8 , 16 , 32 ] ) ( "randomUppercaseString generates lowercase string of %i characters" , ( n : number ) => {
57- const rand1 = randomUppercaseString ( n ) ;
58- const rand2 = randomUppercaseString ( n ) ;
82+ it ( "throws if given character set more than 256 characters" , ( ) => {
83+ const charSet = Array . from ( { length : 257 } , ( _ , i ) => "a" ) . join ( "" ) ;
5984
60- expect ( rand1 ) . not . toEqual ( rand2 ) ;
85+ expect ( ( ) => secureRandomStringFrom ( 8 , charSet ) ) . toThrow ( ) ;
86+ } ) ;
6187
62- expect ( rand1 ) . toHaveLength ( n ) ;
88+ it ( "throws if given length less than 1" , ( ) => {
89+ expect ( ( ) => secureRandomStringFrom ( 0 , "abc" ) ) . toThrow ( ) ;
90+ } ) ;
91+
92+ it ( "throws if given length more than 32768" , ( ) => {
93+ expect ( ( ) => secureRandomStringFrom ( 32769 , "abc" ) ) . toThrow ( ) ;
94+ } ) ;
6395
64- expect ( rand1 . toUpperCase ( ) ) . toEqual ( rand1 ) ;
96+ it ( "asks for more entropy if given entropy is unusable" , ( ) => {
97+ // This is testing the internal implementation details of the function rather
98+ // than strictly the public API. The intention is to have some assertion that
99+ // the rejection sampling to make the distribution even over all possible characters
100+ // is doing what it's supposed to do.
101+
102+ // mock once to fill with 255 the first time: 255 should be unusable because
103+ // we give 10 possible characters below and 256 is not evenly divisible by 10, so
104+ // this should force it to call for more entropy.
105+ jest . spyOn ( globalThis . crypto , "getRandomValues" ) . mockImplementationOnce ( ( arr ) => {
106+ if ( arr === null ) throw new Error ( "Buffer is null" ) ;
107+ new Uint8Array ( arr . buffer ) . fill ( 255 ) ;
108+ return arr ;
109+ } ) ;
110+
111+ secureRandomStringFrom ( 8 , "0123456789" ) ;
112+ expect ( globalThis . crypto . getRandomValues ) . toHaveBeenCalledTimes ( 2 ) ;
65113 } ) ;
66114} ) ;
0 commit comments