github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/config_test.go (about)

     1  /*
     2   * Copyright (c) 2014, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package psiphon
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"testing"
    29  
    30  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    31  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    32  	"github.com/stretchr/testify/suite"
    33  )
    34  
    35  type ConfigTestSuite struct {
    36  	suite.Suite
    37  	confStubBlob      []byte
    38  	requiredFields    []string
    39  	nonRequiredFields []string
    40  	testDirectory     string
    41  }
    42  
    43  func (suite *ConfigTestSuite) SetupSuite() {
    44  	suite.confStubBlob = []byte(`
    45  	{
    46  	    "PropagationChannelId" : "<placeholder>",
    47  	    "SponsorId" : "<placeholder>",
    48  	    "LocalHttpProxyPort" : 8080,
    49  	    "LocalSocksProxyPort" : 1080
    50  	}
    51  	`)
    52  
    53  	var obj map[string]interface{}
    54  	json.Unmarshal(suite.confStubBlob, &obj)
    55  
    56  	// Use a temporary directory for the data root directory so any artifacts
    57  	// created by config.Commit() can be cleaned up.
    58  
    59  	testDirectory, err := ioutil.TempDir("", "psiphon-config-test")
    60  	if err != nil {
    61  		suite.T().Fatalf("TempDir failed: %s\n", err)
    62  	}
    63  	suite.testDirectory = testDirectory
    64  	obj["DataRootDirectory"] = testDirectory
    65  
    66  	suite.confStubBlob, err = json.Marshal(obj)
    67  	if err != nil {
    68  		suite.T().Fatalf("Marshal failed: %s\n", err)
    69  	}
    70  
    71  	for k, v := range obj {
    72  		if k == "DataRootDirectory" {
    73  			// skip
    74  		} else if v == "<placeholder>" {
    75  			suite.requiredFields = append(suite.requiredFields, k)
    76  		} else {
    77  			suite.nonRequiredFields = append(suite.nonRequiredFields, k)
    78  		}
    79  	}
    80  }
    81  
    82  func (suite *ConfigTestSuite) TearDownSuite() {
    83  	if common.FileExists(suite.testDirectory) {
    84  		err := os.RemoveAll(suite.testDirectory)
    85  		if err != nil {
    86  			suite.T().Fatalf("Failed to remove test directory %s: %s", suite.testDirectory, err.Error())
    87  		}
    88  	} else {
    89  		suite.T().Fatalf("Test directory not found: %s", suite.testDirectory)
    90  	}
    91  }
    92  
    93  func TestConfigTestSuite(t *testing.T) {
    94  	suite.Run(t, new(ConfigTestSuite))
    95  }
    96  
    97  // Tests good config
    98  func (suite *ConfigTestSuite) Test_LoadConfig_BasicGood() {
    99  	config, err := LoadConfig(suite.confStubBlob)
   100  	if err == nil {
   101  		err = config.Commit(false)
   102  	}
   103  	suite.Nil(err, "a basic config should succeed")
   104  }
   105  
   106  // Tests non-JSON file contents
   107  func (suite *ConfigTestSuite) Test_LoadConfig_BadFileContents() {
   108  	_, err := LoadConfig([]byte(`this is not JSON`))
   109  	suite.NotNil(err, "bytes that are not JSON at all should give an error")
   110  }
   111  
   112  // Tests config file with JSON contents that don't match our structure
   113  func (suite *ConfigTestSuite) Test_LoadConfig_BadJson() {
   114  	var testObj map[string]interface{}
   115  	var testObjJSON []byte
   116  
   117  	// JSON with none of our fields
   118  	//
   119  	// DataRootDirectory must to be set to avoid a migration in the current
   120  	// working directory.
   121  	config, err := LoadConfig([]byte(
   122  		fmt.Sprintf(
   123  			`{"f1": 11, "f2": "two", "DataRootDirectory" : %s}`,
   124  			suite.testDirectory)))
   125  	if err == nil {
   126  		err = config.Commit(false)
   127  	}
   128  	suite.NotNil(err, "JSON with none of our fields should fail")
   129  
   130  	// Test all required fields
   131  	for _, field := range suite.requiredFields {
   132  		// Missing a required field
   133  		json.Unmarshal(suite.confStubBlob, &testObj)
   134  		delete(testObj, field)
   135  		testObjJSON, _ = json.Marshal(testObj)
   136  		config, err = LoadConfig(testObjJSON)
   137  		if err == nil {
   138  			err = config.Commit(false)
   139  		}
   140  		suite.NotNil(err, "JSON with one of our required fields missing should fail: %s", field)
   141  
   142  		// Bad type for required field
   143  		json.Unmarshal(suite.confStubBlob, &testObj)
   144  		testObj[field] = false // basically guessing a wrong type
   145  		testObjJSON, _ = json.Marshal(testObj)
   146  		config, err = LoadConfig(testObjJSON)
   147  		if err == nil {
   148  			err = config.Commit(false)
   149  		}
   150  		suite.NotNil(err, "JSON with one of our required fields with the wrong type should fail: %s", field)
   151  
   152  		// One of our required fields is null
   153  		json.Unmarshal(suite.confStubBlob, &testObj)
   154  		testObj[field] = nil
   155  		testObjJSON, _ = json.Marshal(testObj)
   156  		config, err = LoadConfig(testObjJSON)
   157  		if err == nil {
   158  			err = config.Commit(false)
   159  		}
   160  		suite.NotNil(err, "JSON with one of our required fields set to null should fail: %s", field)
   161  
   162  		// One of our required fields is an empty string
   163  		json.Unmarshal(suite.confStubBlob, &testObj)
   164  		testObj[field] = ""
   165  		testObjJSON, _ = json.Marshal(testObj)
   166  		config, err = LoadConfig(testObjJSON)
   167  		if err == nil {
   168  			err = config.Commit(false)
   169  		}
   170  		suite.NotNil(err, "JSON with one of our required fields set to an empty string should fail: %s", field)
   171  	}
   172  
   173  	// Test optional fields
   174  	for _, field := range suite.nonRequiredFields {
   175  		// Has incorrect type for optional field
   176  		json.Unmarshal(suite.confStubBlob, &testObj)
   177  		testObj[field] = false // basically guessing a wrong type
   178  		testObjJSON, _ = json.Marshal(testObj)
   179  		config, err = LoadConfig(testObjJSON)
   180  		if err == nil {
   181  			err = config.Commit(false)
   182  		}
   183  		suite.NotNil(err, "JSON with one of our optional fields with the wrong type should fail: %s", field)
   184  	}
   185  }
   186  
   187  // Tests config file with JSON contents that don't match our structure
   188  func (suite *ConfigTestSuite) Test_LoadConfig_GoodJson() {
   189  	var testObj map[string]interface{}
   190  	var testObjJSON []byte
   191  
   192  	// TODO: Test that the config actually gets the values we expect?
   193  
   194  	// Has all of our required fields, but no optional fields
   195  	json.Unmarshal(suite.confStubBlob, &testObj)
   196  	for i := range suite.nonRequiredFields {
   197  		delete(testObj, suite.nonRequiredFields[i])
   198  	}
   199  	testObjJSON, _ = json.Marshal(testObj)
   200  	config, err := LoadConfig(testObjJSON)
   201  	if err == nil {
   202  		err = config.Commit(false)
   203  	}
   204  	suite.Nil(err, "JSON with good values for our required fields but no optional fields should succeed")
   205  
   206  	// Has all of our required fields, and all optional fields
   207  	config, err = LoadConfig(suite.confStubBlob)
   208  	if err == nil {
   209  		err = config.Commit(false)
   210  	}
   211  	suite.Nil(err, "JSON with all good values for required and optional fields should succeed")
   212  
   213  	// Has null for optional fields
   214  	json.Unmarshal(suite.confStubBlob, &testObj)
   215  	for i := range suite.nonRequiredFields {
   216  		testObj[suite.nonRequiredFields[i]] = nil
   217  	}
   218  	testObjJSON, _ = json.Marshal(testObj)
   219  	config, err = LoadConfig(testObjJSON)
   220  	if err == nil {
   221  		err = config.Commit(false)
   222  	}
   223  	suite.Nil(err, "JSON with null for optional values should succeed")
   224  }
   225  
   226  func (suite *ConfigTestSuite) Test_LoadConfig_Migrate() {
   227  	oslFiles := []FileTree{
   228  		{
   229  			Name: "osl-registry",
   230  		},
   231  		{
   232  			Name: "osl-registry.cached",
   233  		},
   234  		{
   235  			Name: "osl-1",
   236  		},
   237  		{
   238  			Name: "osl-1.part",
   239  		}}
   240  
   241  	nonOSLFile := FileTree{
   242  		Name: "should_not_be_deleted",
   243  		Children: []FileTree{
   244  			{
   245  				Name: "should_not_be_deleted",
   246  			},
   247  		},
   248  	}
   249  
   250  	// Test where OSL directory is not deleted after migration because
   251  	// it contains non-OSL files.
   252  	LoadConfigMigrateTest(append(oslFiles, nonOSLFile), &nonOSLFile, suite)
   253  
   254  	// Test where OSL directory is deleted after migration because it only
   255  	// contained OSL files.
   256  	LoadConfigMigrateTest(oslFiles, nil, suite)
   257  }
   258  
   259  // Test when migrating from old config fields results in filesystem changes.
   260  func LoadConfigMigrateTest(oslDirChildrenPreMigration []FileTree, oslDirChildrenPostMigration *FileTree, suite *ConfigTestSuite) {
   261  	// This test needs its own temporary directory because a previous test may
   262  	// have paved the file which signals that migration has already been
   263  	// completed.
   264  	testDirectory, err := ioutil.TempDir("", "psiphon-config-migration-test")
   265  	if err != nil {
   266  		suite.T().Fatalf("TempDir failed: %s\n", err)
   267  	}
   268  
   269  	defer func() {
   270  		if common.FileExists(testDirectory) {
   271  			err := os.RemoveAll(testDirectory)
   272  			if err != nil {
   273  				suite.T().Fatalf("Failed to remove test directory %s: %s", testDirectory, err.Error())
   274  			}
   275  		}
   276  	}()
   277  
   278  	// Pre migration files and directories
   279  	oldDataStoreDirectory := filepath.Join(testDirectory, "datastore_old")
   280  	oldRemoteServerListname := "rsl"
   281  	oldObfuscatedServerListDirectoryName := "obfuscated_server_list"
   282  	oldObfuscatedServerListDirectory := filepath.Join(testDirectory, oldObfuscatedServerListDirectoryName)
   283  	oldUpgradeDownloadFilename := "upgrade"
   284  	oldRotatingNoticesFilename := "rotating_notices"
   285  	oldHomepageNoticeFilename := "homepage"
   286  
   287  	// Post migration data root directory
   288  	testDataRootDirectory := filepath.Join(testDirectory, "data_root_directory")
   289  
   290  	oldFileTree := FileTree{
   291  		Name: testDirectory,
   292  		Children: []FileTree{
   293  			{
   294  				Name: "datastore_old",
   295  				Children: []FileTree{
   296  					{
   297  						Name: "psiphon.boltdb",
   298  					},
   299  					{
   300  						Name: "psiphon.boltdb.lock",
   301  					},
   302  					{
   303  						Name: "non_tunnel_core_file_should_not_be_migrated",
   304  					},
   305  				},
   306  			},
   307  			{
   308  				Name: oldRemoteServerListname,
   309  			},
   310  			{
   311  				Name: oldRemoteServerListname + ".part",
   312  			},
   313  			{
   314  				Name: oldRemoteServerListname + ".part.etag",
   315  			},
   316  			{
   317  				Name:     oldObfuscatedServerListDirectoryName,
   318  				Children: oslDirChildrenPreMigration,
   319  			},
   320  			{
   321  				Name: oldRotatingNoticesFilename,
   322  			},
   323  			{
   324  				Name: oldRotatingNoticesFilename + ".1",
   325  			},
   326  			{
   327  				Name: oldHomepageNoticeFilename,
   328  			},
   329  			{
   330  				Name: oldUpgradeDownloadFilename,
   331  			},
   332  			{
   333  				Name: oldUpgradeDownloadFilename + ".1234",
   334  			},
   335  			{
   336  				Name: oldUpgradeDownloadFilename + ".1234.part",
   337  			},
   338  			{
   339  				Name: oldUpgradeDownloadFilename + ".1234.part.etag",
   340  			},
   341  			{
   342  				Name: "data_root_directory",
   343  				Children: []FileTree{
   344  					{
   345  						Name: "non_tunnel_core_file_should_not_be_clobbered",
   346  					},
   347  				},
   348  			},
   349  		},
   350  	}
   351  
   352  	// Write test files
   353  	traverseFileTree(func(tree FileTree, path string) {
   354  		if tree.Children == nil || len(tree.Children) == 0 {
   355  			if !common.FileExists(path) {
   356  				f, err := os.Create(path)
   357  				if err != nil {
   358  					suite.T().Fatalf("Failed to create test file %s with error: %s", path, err.Error())
   359  				}
   360  				f.Close()
   361  			}
   362  		} else {
   363  			if !common.FileExists(path) {
   364  				err := os.Mkdir(path, os.ModePerm)
   365  				if err != nil {
   366  					suite.T().Fatalf("Failed to create test directory %s with error: %s", path, err.Error())
   367  				}
   368  			}
   369  		}
   370  	}, "", oldFileTree)
   371  
   372  	// Create config with legacy config values
   373  	config := &Config{
   374  		DataRootDirectory:                            testDataRootDirectory,
   375  		MigrateRotatingNoticesFilename:               filepath.Join(testDirectory, oldRotatingNoticesFilename),
   376  		MigrateHomepageNoticesFilename:               filepath.Join(testDirectory, oldHomepageNoticeFilename),
   377  		MigrateDataStoreDirectory:                    oldDataStoreDirectory,
   378  		PropagationChannelId:                         "ABCDEFGH",
   379  		SponsorId:                                    "12345678",
   380  		LocalSocksProxyPort:                          0,
   381  		LocalHttpProxyPort:                           0,
   382  		MigrateRemoteServerListDownloadFilename:      filepath.Join(testDirectory, oldRemoteServerListname),
   383  		MigrateObfuscatedServerListDownloadDirectory: oldObfuscatedServerListDirectory,
   384  		MigrateUpgradeDownloadFilename:               filepath.Join(testDirectory, oldUpgradeDownloadFilename),
   385  	}
   386  
   387  	// Commit config, this is where file migration happens
   388  	err = config.Commit(true)
   389  	if err != nil {
   390  		suite.T().Fatal("Error committing config:", err)
   391  		return
   392  	}
   393  
   394  	expectedNewTree := FileTree{
   395  		Name: testDirectory,
   396  		Children: []FileTree{
   397  			{
   398  				Name: "data_root_directory",
   399  				Children: []FileTree{
   400  					{
   401  						Name: "non_tunnel_core_file_should_not_be_clobbered",
   402  					},
   403  					{
   404  						Name: "ca.psiphon.PsiphonTunnel.tunnel-core",
   405  						Children: []FileTree{
   406  							{
   407  								Name: "migration_complete",
   408  							},
   409  							{
   410  								Name: "remote_server_list",
   411  							},
   412  							{
   413  								Name: "remote_server_list.part",
   414  							},
   415  							{
   416  								Name: "remote_server_list.part.etag",
   417  							},
   418  							{
   419  								Name: "datastore",
   420  								Children: []FileTree{
   421  									{
   422  										Name: "psiphon.boltdb",
   423  									},
   424  									{
   425  										Name: "psiphon.boltdb.lock",
   426  									},
   427  								},
   428  							},
   429  							{
   430  								Name: "osl",
   431  								Children: []FileTree{
   432  									{
   433  										Name: "osl-registry",
   434  									},
   435  									{
   436  										Name: "osl-registry.cached",
   437  									},
   438  									{
   439  										Name: "osl-1",
   440  									},
   441  									{
   442  										Name: "osl-1.part",
   443  									},
   444  								},
   445  							},
   446  							{
   447  								Name: "upgrade",
   448  							},
   449  							{
   450  								Name: "upgrade.1234",
   451  							},
   452  							{
   453  								Name: "upgrade.1234.part",
   454  							},
   455  							{
   456  								Name: "upgrade.1234.part.etag",
   457  							},
   458  							{
   459  								Name: "notices",
   460  							},
   461  							{
   462  								Name: "notices.1",
   463  							},
   464  							{
   465  								Name: "homepage",
   466  							},
   467  						},
   468  					},
   469  				},
   470  			},
   471  			{
   472  				Name: "datastore_old",
   473  				Children: []FileTree{
   474  					{
   475  						Name: "non_tunnel_core_file_should_not_be_migrated",
   476  					},
   477  				},
   478  			},
   479  		},
   480  	}
   481  
   482  	// The OSL directory will have been deleted if it has no children after
   483  	// migration.
   484  	if oslDirChildrenPostMigration != nil {
   485  		oslDir := FileTree{
   486  			Name:     oldObfuscatedServerListDirectoryName,
   487  			Children: []FileTree{*oslDirChildrenPostMigration},
   488  		}
   489  		expectedNewTree.Children = append(expectedNewTree.Children, oslDir)
   490  	}
   491  
   492  	// Read the test directory into a file tree
   493  	testDirectoryTree, err := buildDirectoryTree("", testDirectory)
   494  	if err != nil {
   495  		suite.T().Fatal("Failed to build directory tree:", err)
   496  	}
   497  
   498  	// Enumerate the file paths, relative to the test directory,
   499  	// of each file in the test directory after migration.
   500  	testDirectoryFilePaths := make(map[string]int)
   501  	traverseFileTree(func(tree FileTree, path string) {
   502  		if val, ok := testDirectoryFilePaths[path]; ok {
   503  			testDirectoryFilePaths[path] = val + 1
   504  		} else {
   505  			testDirectoryFilePaths[path] = 1
   506  		}
   507  	}, "", *testDirectoryTree)
   508  
   509  	// Enumerate the file paths, relative to the test directory,
   510  	// of each file we expect to exist in the test directory tree
   511  	// after migration.
   512  	expectedTestDirectoryFilePaths := make(map[string]int)
   513  	traverseFileTree(func(tree FileTree, path string) {
   514  		if val, ok := expectedTestDirectoryFilePaths[path]; ok {
   515  			expectedTestDirectoryFilePaths[path] = val + 1
   516  		} else {
   517  			expectedTestDirectoryFilePaths[path] = 1
   518  		}
   519  	}, "", expectedNewTree)
   520  
   521  	// The set of expected file paths and set of actual  file paths should be
   522  	// identical.
   523  
   524  	for k, _ := range expectedTestDirectoryFilePaths {
   525  		_, ok := testDirectoryFilePaths[k]
   526  		if ok {
   527  			// Prevent redundant checks
   528  			delete(testDirectoryFilePaths, k)
   529  		} else {
   530  			suite.T().Errorf("Expected %s to exist in directory", k)
   531  		}
   532  	}
   533  
   534  	for k, _ := range testDirectoryFilePaths {
   535  		if _, ok := expectedTestDirectoryFilePaths[k]; !ok {
   536  			suite.T().Errorf("%s in directory but not expected", k)
   537  		}
   538  	}
   539  }
   540  
   541  // FileTree represents a file or directory in a file tree.
   542  // There is no need to distinguish between the two in our tests.
   543  type FileTree struct {
   544  	Name     string
   545  	Children []FileTree
   546  }
   547  
   548  // traverseFileTree traverses a file tree and emits the filepath of each node.
   549  //
   550  // For example:
   551  //
   552  //   a
   553  //   ├── b
   554  //   │   ├── 1
   555  //   │   └── 2
   556  //   └── c
   557  //       └── 3
   558  //
   559  // Will result in: ["a", "a/b", "a/b/1", "a/b/2", "a/c", "a/c/3"].
   560  func traverseFileTree(f func(node FileTree, nodePath string), basePath string, tree FileTree) {
   561  	filePath := filepath.Join(basePath, tree.Name)
   562  	f(tree, filePath)
   563  	if tree.Children == nil || len(tree.Children) == 0 {
   564  		return
   565  	}
   566  	for _, childTree := range tree.Children {
   567  		traverseFileTree(f, filePath, childTree)
   568  	}
   569  }
   570  
   571  // buildDirectoryTree creates a file tree, with the given directory as its root,
   572  // representing the directory structure that exists relative to the given directory.
   573  func buildDirectoryTree(basePath, directoryName string) (*FileTree, error) {
   574  
   575  	tree := &FileTree{
   576  		Name:     directoryName,
   577  		Children: nil,
   578  	}
   579  
   580  	dirPath := filepath.Join(basePath, directoryName)
   581  	files, err := ioutil.ReadDir(dirPath)
   582  	if err != nil {
   583  		return nil, errors.Tracef("Failed to read directory %s with error: %s", dirPath, err.Error())
   584  	}
   585  
   586  	if len(files) > 0 {
   587  		for _, file := range files {
   588  			if file.IsDir() {
   589  				filePath := filepath.Join(basePath, directoryName)
   590  				childTree, err := buildDirectoryTree(filePath, file.Name())
   591  				if err != nil {
   592  					return nil, err
   593  				}
   594  				tree.Children = append(tree.Children, *childTree)
   595  			} else {
   596  				tree.Children = append(tree.Children, FileTree{
   597  					Name:     file.Name(),
   598  					Children: nil,
   599  				})
   600  			}
   601  		}
   602  	}
   603  
   604  	return tree, nil
   605  }