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 }