decred.org/dcrdex@v1.0.5/dex/encode/passbytes_test.go (about) 1 package encode 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "math/rand" 8 "testing" 9 "time" 10 ) 11 12 const ( 13 digits = "0123456789" 14 specials = "~=+%^*/()[]{}/!@#$?|\"\\&-_<>',.;" 15 all = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + digits + specials 16 ) 17 18 func randomString(length int) string { 19 if length == 0 { 20 return "" 21 } 22 23 shuffle := func(b []byte) string { 24 rand.Shuffle(len(b), func(i, j int) { 25 b[i], b[j] = b[j], b[i] 26 }) 27 return string(b) 28 } 29 30 rand.Seed(time.Now().UnixNano()) 31 buf := make([]byte, length) 32 33 if length <= 2 { 34 for i := 0; i < length; i++ { 35 buf[i] = all[rand.Intn(len(all))] // include additional random char from `all` 36 } 37 return shuffle(buf) 38 } 39 40 buf[0] = digits[rand.Intn(len(digits))] // include at least 1 digit 41 buf[1] = specials[rand.Intn(len(specials))] // include at least 1 special char 42 for i := 2; i < length; i++ { 43 buf[i] = all[rand.Intn(len(all))] // include additional random char from `all` 44 } 45 46 return shuffle(buf) 47 } 48 49 // TestMarshalUnmarshal generates random strings and ensures that, with the 50 // exception of strings containing invalid chars such as `\xe2`, 51 // - each string can be marshalled directly into a JSON-encoded byte slice, 52 // - the JSON-encoded byte slice can be unmarshalled into a `PassBytes` ptr, 53 // - the string representation of the `PassBytes` is the same as the original 54 // string. 55 // 56 // TestMarshalUnmarshal also creates a test object with each string 57 // and ensures that 58 // - the test object can be marshalled into a JSON-encoded byte slice, 59 // - the JSON-encoded byte slice can be unmarshalled into another object 60 // having a `PassBytes` field, 61 // - the string value of the second object's PassBytes field is the same as 62 // the original string. 63 // 64 // TestMarshalUnmarshal also ensures that marshalling strings with invalid chars 65 // produces an error. 66 func TestMarshalUnmarshal(t *testing.T) { 67 checkDirectUnmarshal := func(password string, expectMarshalError bool) error { 68 // marshal to json first 69 pwJSON, err := json.Marshal(password) 70 if err != nil { 71 return err 72 } 73 // unmarshal back from json 74 var pwFromJSON PassBytes 75 if err = json.Unmarshal(pwJSON, &pwFromJSON); err != nil { 76 return err 77 } 78 // confirm accuracy of unmarshal 79 unmarshalledPassword := string(pwFromJSON) 80 if password != unmarshalledPassword { 81 return fmt.Errorf("original pw: %q marshalled to %q unmarshalled to %q", 82 password, string(pwJSON), unmarshalledPassword) 83 } 84 return nil 85 } 86 87 checkUnmarshalAsBodyProperty := func(password string, expectMarshalError bool) error { 88 // marshal to json first 89 originalObj := struct { 90 Password string `json:"pass"` 91 }{ 92 Password: password, 93 } 94 jsonObj, err := json.Marshal(originalObj) 95 if err != nil { 96 return err 97 } 98 // unmarshal back from json 99 var restoredObj = new(struct { 100 Password PassBytes `json:"pass"` 101 }) 102 if err = json.Unmarshal(jsonObj, restoredObj); err != nil { 103 return err 104 } 105 // confirm accuracy of unmarshal 106 unmarshalledPassword := string(restoredObj.Password) 107 if password != unmarshalledPassword { 108 return fmt.Errorf("original obj: %v (password %q) marshalled to %s and unmarshalled to %v (password %q)", 109 originalObj, password, string(jsonObj), restoredObj, unmarshalledPassword) 110 } 111 return nil 112 } 113 114 // test with 100 randomly generated strings of length between 0 and 19 chars 115 for i := 0; i < 100; i++ { 116 s := randomString(i % 20) 117 expectMarshalError := !json.Valid([]byte(s)) // expect marshal error if s contains invalid chars 118 if err := checkDirectUnmarshal(s, expectMarshalError); err != nil { 119 t.Fatal(err) 120 } 121 if err := checkUnmarshalAsBodyProperty(s, expectMarshalError); err != nil { 122 t.Fatal(err) 123 } 124 } 125 } 126 127 // jsonString represents a JSON-encoded string, which may or may not be valid. 128 type jsonString string 129 130 // bytes returns `js.jsonPass` converted to []byte. 131 func (js jsonString) bytes() []byte { 132 return []byte(js) 133 } 134 135 // string attempts to unmarshal `js.jsonPass` into a string. Returns the 136 // unmarshalled string or an error if unmarshalling fails. 137 func (js jsonString) string() (s string, err error) { 138 err = json.Unmarshal(js.bytes(), &s) 139 return s, err 140 } 141 142 // TestUnmarshalJSONEncodedStrings defines a collection of valid and invalid 143 // JSON-encoded strings (aka `jsonString`s). 144 // Each jsonString is converted to JSON-encoded data using `[]byte(s)` and 145 // unmarshalled into a `PassBytes` ptr. 146 // Ensures successful unmarshalling for valid JSON-encoded strings and errors 147 // for invalid JSON-encoded strings. 148 func TestUnmarshalJSONEncodedStrings(t *testing.T) { 149 // Test with pre-defined inputs and expectations. Inputs will be converted 150 // to bytes (not json-marshalled), so we'd expect inputs that are not valid 151 // json-encoded strings to fail. 152 expectationTests := []struct { 153 name string 154 jsonPass jsonString 155 expectErr bool 156 }{ 157 // valid json-encoded strings 158 { 159 name: "empty string", 160 jsonPass: `""`, 161 expectErr: false, 162 }, 163 { 164 name: "qutf-8 string", 165 jsonPass: `"@123"`, 166 expectErr: false, 167 }, 168 { 169 name: "string with escaped char (\")", 170 jsonPass: `"quote\"embedded"`, 171 expectErr: false, 172 }, 173 { 174 name: "unicode character", 175 jsonPass: `"\u5f5b"`, 176 expectErr: false, 177 }, 178 { 179 name: "unicode surrogate pair", 180 jsonPass: `"\uD800\uDC00"`, 181 expectErr: false, 182 }, 183 { 184 name: "valid non-utf8 characters", 185 jsonPass: `"√ç∂"`, 186 expectErr: false, 187 }, 188 189 // invalid json strings 190 { 191 name: "unquoted empty string", 192 jsonPass: "", 193 expectErr: true, 194 }, 195 { 196 name: "unquoted UTF8 string", 197 jsonPass: `@123`, 198 expectErr: true, 199 }, 200 { 201 name: `quoted string with unescaped " char`, 202 jsonPass: `"quote"embedded"`, 203 expectErr: true, 204 }, 205 { 206 name: `invalid unicode character (\U instead of \u)`, 207 jsonPass: `"\UD800"`, 208 expectErr: true, 209 }, 210 { 211 name: `invalid unicode surrogate pair (\U instead of \u)`, 212 jsonPass: `"\UD800\UDC00"`, 213 expectErr: true, 214 }, 215 } 216 217 for _, test := range expectationTests { 218 var unmarshalledPassBytes PassBytes 219 err := json.Unmarshal(test.jsonPass.bytes(), &unmarshalledPassBytes) 220 if test.expectErr && err != nil { 221 continue 222 } 223 if test.expectErr { 224 t.Fatalf("%s -> %q: expected error but got nil", test.name, test.jsonPass) 225 } 226 if err != nil { 227 t.Fatalf("%s -> %q: unexpected unmarshal error: %v", test.name, test.jsonPass, err) 228 } 229 230 // If we got here, we expect unmarshalling to have been successful, 231 // confirm that the unmarshalled password is accurate. 232 actualPassword, err := test.jsonPass.string() 233 if err != nil { 234 t.Fatalf("%s: unexpected error getting actual password string from JSON-encoded string (%q): %v", 235 test.name, test.jsonPass, err) 236 } 237 unmarshalunmarshalledPassword := string(unmarshalledPassBytes) 238 if actualPassword != unmarshalunmarshalledPassword { 239 t.Fatalf("%s: expected %q, got %q", test.name, actualPassword, unmarshalunmarshalledPassword) 240 } 241 } 242 } 243 244 // TestMarshal ensures that `PassBytes` can be properly marshalled into raw 245 // bytes which can be unmarshalled back into a `PassBytes` ptr either directly 246 // or as part of a struct object. 247 func TestMarshal(t *testing.T) { 248 pb := PassBytes("def") 249 pbRawBytes, err := json.Marshal(pb) 250 if err != nil { 251 t.Fatalf("cannot marshal PassBytes: %v", err) 252 } 253 254 // check if pbRawBytes can be successfully unmarshalled into *PassBytes 255 var pbUnmarshalled PassBytes 256 err = json.Unmarshal(pbRawBytes, &pbUnmarshalled) 257 if err != nil { 258 t.Fatalf("cannot unmarshal the rawbytes of a PassBytes: %v", err) 259 } 260 261 if !bytes.Equal(pb, pbUnmarshalled) { 262 t.Fatalf("expected original passbytes %x to equal unmarshalled passbytes %s", pb, pbUnmarshalled) 263 } 264 }