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  }