/*
* Copyright (c) 2014, Psiphon Inc.
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
package psiphon
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
"github.com/stretchr/testify/suite"
)
type ConfigTestSuite struct {
suite.Suite
confStubBlob []byte
requiredFields []string
nonRequiredFields []string
testDirectory string
}
func (suite *ConfigTestSuite) SetupSuite() {
suite.confStubBlob = []byte(`
{
"PropagationChannelId" : "",
"SponsorId" : "",
"LocalHttpProxyPort" : 8080,
"LocalSocksProxyPort" : 1080
}
`)
var obj map[string]interface{}
json.Unmarshal(suite.confStubBlob, &obj)
// Use a temporary directory for the data root directory so any artifacts
// created by config.Commit() can be cleaned up.
testDirectory, err := ioutil.TempDir("", "psiphon-config-test")
if err != nil {
suite.T().Fatalf("TempDir failed: %s\n", err)
}
suite.testDirectory = testDirectory
obj["DataRootDirectory"] = testDirectory
suite.confStubBlob, err = json.Marshal(obj)
if err != nil {
suite.T().Fatalf("Marshal failed: %s\n", err)
}
for k, v := range obj {
if k == "DataRootDirectory" {
// skip
} else if v == "" {
suite.requiredFields = append(suite.requiredFields, k)
} else {
suite.nonRequiredFields = append(suite.nonRequiredFields, k)
}
}
}
func (suite *ConfigTestSuite) TearDownSuite() {
if common.FileExists(suite.testDirectory) {
err := os.RemoveAll(suite.testDirectory)
if err != nil {
suite.T().Fatalf("Failed to remove test directory %s: %s", suite.testDirectory, err.Error())
}
} else {
suite.T().Fatalf("Test directory not found: %s", suite.testDirectory)
}
}
func TestConfigTestSuite(t *testing.T) {
suite.Run(t, new(ConfigTestSuite))
}
// Tests good config
func (suite *ConfigTestSuite) Test_LoadConfig_BasicGood() {
config, err := LoadConfig(suite.confStubBlob)
if err == nil {
err = config.Commit(false)
}
suite.Nil(err, "a basic config should succeed")
}
// Tests non-JSON file contents
func (suite *ConfigTestSuite) Test_LoadConfig_BadFileContents() {
_, err := LoadConfig([]byte(`this is not JSON`))
suite.NotNil(err, "bytes that are not JSON at all should give an error")
}
// Tests config file with JSON contents that don't match our structure
func (suite *ConfigTestSuite) Test_LoadConfig_BadJson() {
var testObj map[string]interface{}
var testObjJSON []byte
// JSON with none of our fields
//
// DataRootDirectory must to be set to avoid a migration in the current
// working directory.
config, err := LoadConfig([]byte(
fmt.Sprintf(
`{"f1": 11, "f2": "two", "DataRootDirectory" : %s}`,
suite.testDirectory)))
if err == nil {
err = config.Commit(false)
}
suite.NotNil(err, "JSON with none of our fields should fail")
// Test all required fields
for _, field := range suite.requiredFields {
// Missing a required field
json.Unmarshal(suite.confStubBlob, &testObj)
delete(testObj, field)
testObjJSON, _ = json.Marshal(testObj)
config, err = LoadConfig(testObjJSON)
if err == nil {
err = config.Commit(false)
}
suite.NotNil(err, "JSON with one of our required fields missing should fail: %s", field)
// Bad type for required field
json.Unmarshal(suite.confStubBlob, &testObj)
testObj[field] = false // basically guessing a wrong type
testObjJSON, _ = json.Marshal(testObj)
config, err = LoadConfig(testObjJSON)
if err == nil {
err = config.Commit(false)
}
suite.NotNil(err, "JSON with one of our required fields with the wrong type should fail: %s", field)
// One of our required fields is null
json.Unmarshal(suite.confStubBlob, &testObj)
testObj[field] = nil
testObjJSON, _ = json.Marshal(testObj)
config, err = LoadConfig(testObjJSON)
if err == nil {
err = config.Commit(false)
}
suite.NotNil(err, "JSON with one of our required fields set to null should fail: %s", field)
// One of our required fields is an empty string
json.Unmarshal(suite.confStubBlob, &testObj)
testObj[field] = ""
testObjJSON, _ = json.Marshal(testObj)
config, err = LoadConfig(testObjJSON)
if err == nil {
err = config.Commit(false)
}
suite.NotNil(err, "JSON with one of our required fields set to an empty string should fail: %s", field)
}
// Test optional fields
for _, field := range suite.nonRequiredFields {
// Has incorrect type for optional field
json.Unmarshal(suite.confStubBlob, &testObj)
testObj[field] = false // basically guessing a wrong type
testObjJSON, _ = json.Marshal(testObj)
config, err = LoadConfig(testObjJSON)
if err == nil {
err = config.Commit(false)
}
suite.NotNil(err, "JSON with one of our optional fields with the wrong type should fail: %s", field)
}
}
// Tests config file with JSON contents that don't match our structure
func (suite *ConfigTestSuite) Test_LoadConfig_GoodJson() {
var testObj map[string]interface{}
var testObjJSON []byte
// TODO: Test that the config actually gets the values we expect?
// Has all of our required fields, but no optional fields
json.Unmarshal(suite.confStubBlob, &testObj)
for i := range suite.nonRequiredFields {
delete(testObj, suite.nonRequiredFields[i])
}
testObjJSON, _ = json.Marshal(testObj)
config, err := LoadConfig(testObjJSON)
if err == nil {
err = config.Commit(false)
}
suite.Nil(err, "JSON with good values for our required fields but no optional fields should succeed")
// Has all of our required fields, and all optional fields
config, err = LoadConfig(suite.confStubBlob)
if err == nil {
err = config.Commit(false)
}
suite.Nil(err, "JSON with all good values for required and optional fields should succeed")
// Has null for optional fields
json.Unmarshal(suite.confStubBlob, &testObj)
for i := range suite.nonRequiredFields {
testObj[suite.nonRequiredFields[i]] = nil
}
testObjJSON, _ = json.Marshal(testObj)
config, err = LoadConfig(testObjJSON)
if err == nil {
err = config.Commit(false)
}
suite.Nil(err, "JSON with null for optional values should succeed")
}
func (suite *ConfigTestSuite) Test_LoadConfig_Migrate() {
oslFiles := []FileTree{
{
Name: "osl-registry",
},
{
Name: "osl-registry.cached",
},
{
Name: "osl-1",
},
{
Name: "osl-1.part",
}}
nonOSLFile := FileTree{
Name: "should_not_be_deleted",
Children: []FileTree{
{
Name: "should_not_be_deleted",
},
},
}
// Test where OSL directory is not deleted after migration because
// it contains non-OSL files.
LoadConfigMigrateTest(append(oslFiles, nonOSLFile), &nonOSLFile, suite)
// Test where OSL directory is deleted after migration because it only
// contained OSL files.
LoadConfigMigrateTest(oslFiles, nil, suite)
}
// Test when migrating from old config fields results in filesystem changes.
func LoadConfigMigrateTest(oslDirChildrenPreMigration []FileTree, oslDirChildrenPostMigration *FileTree, suite *ConfigTestSuite) {
// This test needs its own temporary directory because a previous test may
// have paved the file which signals that migration has already been
// completed.
testDirectory, err := ioutil.TempDir("", "psiphon-config-migration-test")
if err != nil {
suite.T().Fatalf("TempDir failed: %s\n", err)
}
defer func() {
if common.FileExists(testDirectory) {
err := os.RemoveAll(testDirectory)
if err != nil {
suite.T().Fatalf("Failed to remove test directory %s: %s", testDirectory, err.Error())
}
}
}()
// Pre migration files and directories
oldDataStoreDirectory := filepath.Join(testDirectory, "datastore_old")
oldRemoteServerListname := "rsl"
oldObfuscatedServerListDirectoryName := "obfuscated_server_list"
oldObfuscatedServerListDirectory := filepath.Join(testDirectory, oldObfuscatedServerListDirectoryName)
oldUpgradeDownloadFilename := "upgrade"
oldRotatingNoticesFilename := "rotating_notices"
oldHomepageNoticeFilename := "homepage"
// Post migration data root directory
testDataRootDirectory := filepath.Join(testDirectory, "data_root_directory")
oldFileTree := FileTree{
Name: testDirectory,
Children: []FileTree{
{
Name: "datastore_old",
Children: []FileTree{
{
Name: "psiphon.boltdb",
},
{
Name: "psiphon.boltdb.lock",
},
{
Name: "non_tunnel_core_file_should_not_be_migrated",
},
},
},
{
Name: oldRemoteServerListname,
},
{
Name: oldRemoteServerListname + ".part",
},
{
Name: oldRemoteServerListname + ".part.etag",
},
{
Name: oldObfuscatedServerListDirectoryName,
Children: oslDirChildrenPreMigration,
},
{
Name: oldRotatingNoticesFilename,
},
{
Name: oldRotatingNoticesFilename + ".1",
},
{
Name: oldHomepageNoticeFilename,
},
{
Name: oldUpgradeDownloadFilename,
},
{
Name: oldUpgradeDownloadFilename + ".1234",
},
{
Name: oldUpgradeDownloadFilename + ".1234.part",
},
{
Name: oldUpgradeDownloadFilename + ".1234.part.etag",
},
{
Name: "data_root_directory",
Children: []FileTree{
{
Name: "non_tunnel_core_file_should_not_be_clobbered",
},
},
},
},
}
// Write test files
traverseFileTree(func(tree FileTree, path string) {
if tree.Children == nil || len(tree.Children) == 0 {
if !common.FileExists(path) {
f, err := os.Create(path)
if err != nil {
suite.T().Fatalf("Failed to create test file %s with error: %s", path, err.Error())
}
f.Close()
}
} else {
if !common.FileExists(path) {
err := os.Mkdir(path, os.ModePerm)
if err != nil {
suite.T().Fatalf("Failed to create test directory %s with error: %s", path, err.Error())
}
}
}
}, "", oldFileTree)
// Create config with legacy config values
config := &Config{
DataRootDirectory: testDataRootDirectory,
MigrateRotatingNoticesFilename: filepath.Join(testDirectory, oldRotatingNoticesFilename),
MigrateHomepageNoticesFilename: filepath.Join(testDirectory, oldHomepageNoticeFilename),
MigrateDataStoreDirectory: oldDataStoreDirectory,
PropagationChannelId: "ABCDEFGH",
SponsorId: "12345678",
LocalSocksProxyPort: 0,
LocalHttpProxyPort: 0,
MigrateRemoteServerListDownloadFilename: filepath.Join(testDirectory, oldRemoteServerListname),
MigrateObfuscatedServerListDownloadDirectory: oldObfuscatedServerListDirectory,
MigrateUpgradeDownloadFilename: filepath.Join(testDirectory, oldUpgradeDownloadFilename),
}
// Commit config, this is where file migration happens
err = config.Commit(true)
if err != nil {
suite.T().Fatal("Error committing config:", err)
return
}
expectedNewTree := FileTree{
Name: testDirectory,
Children: []FileTree{
{
Name: "data_root_directory",
Children: []FileTree{
{
Name: "non_tunnel_core_file_should_not_be_clobbered",
},
{
Name: "ca.psiphon.PsiphonTunnel.tunnel-core",
Children: []FileTree{
{
Name: "migration_complete",
},
{
Name: "remote_server_list",
},
{
Name: "remote_server_list.part",
},
{
Name: "remote_server_list.part.etag",
},
{
Name: "datastore",
Children: []FileTree{
{
Name: "psiphon.boltdb",
},
{
Name: "psiphon.boltdb.lock",
},
},
},
{
Name: "osl",
Children: []FileTree{
{
Name: "osl-registry",
},
{
Name: "osl-registry.cached",
},
{
Name: "osl-1",
},
{
Name: "osl-1.part",
},
},
},
{
Name: "upgrade",
},
{
Name: "upgrade.1234",
},
{
Name: "upgrade.1234.part",
},
{
Name: "upgrade.1234.part.etag",
},
{
Name: "notices",
},
{
Name: "notices.1",
},
{
Name: "homepage",
},
},
},
},
},
{
Name: "datastore_old",
Children: []FileTree{
{
Name: "non_tunnel_core_file_should_not_be_migrated",
},
},
},
},
}
// The OSL directory will have been deleted if it has no children after
// migration.
if oslDirChildrenPostMigration != nil {
oslDir := FileTree{
Name: oldObfuscatedServerListDirectoryName,
Children: []FileTree{*oslDirChildrenPostMigration},
}
expectedNewTree.Children = append(expectedNewTree.Children, oslDir)
}
// Read the test directory into a file tree
testDirectoryTree, err := buildDirectoryTree("", testDirectory)
if err != nil {
suite.T().Fatal("Failed to build directory tree:", err)
}
// Enumerate the file paths, relative to the test directory,
// of each file in the test directory after migration.
testDirectoryFilePaths := make(map[string]int)
traverseFileTree(func(tree FileTree, path string) {
if val, ok := testDirectoryFilePaths[path]; ok {
testDirectoryFilePaths[path] = val + 1
} else {
testDirectoryFilePaths[path] = 1
}
}, "", *testDirectoryTree)
// Enumerate the file paths, relative to the test directory,
// of each file we expect to exist in the test directory tree
// after migration.
expectedTestDirectoryFilePaths := make(map[string]int)
traverseFileTree(func(tree FileTree, path string) {
if val, ok := expectedTestDirectoryFilePaths[path]; ok {
expectedTestDirectoryFilePaths[path] = val + 1
} else {
expectedTestDirectoryFilePaths[path] = 1
}
}, "", expectedNewTree)
// The set of expected file paths and set of actual file paths should be
// identical.
for k, _ := range expectedTestDirectoryFilePaths {
_, ok := testDirectoryFilePaths[k]
if ok {
// Prevent redundant checks
delete(testDirectoryFilePaths, k)
} else {
suite.T().Errorf("Expected %s to exist in directory", k)
}
}
for k, _ := range testDirectoryFilePaths {
if _, ok := expectedTestDirectoryFilePaths[k]; !ok {
suite.T().Errorf("%s in directory but not expected", k)
}
}
}
// FileTree represents a file or directory in a file tree.
// There is no need to distinguish between the two in our tests.
type FileTree struct {
Name string
Children []FileTree
}
// traverseFileTree traverses a file tree and emits the filepath of each node.
//
// For example:
//
// a
// ├── b
// │ ├── 1
// │ └── 2
// └── c
// └── 3
//
// Will result in: ["a", "a/b", "a/b/1", "a/b/2", "a/c", "a/c/3"].
func traverseFileTree(f func(node FileTree, nodePath string), basePath string, tree FileTree) {
filePath := filepath.Join(basePath, tree.Name)
f(tree, filePath)
if tree.Children == nil || len(tree.Children) == 0 {
return
}
for _, childTree := range tree.Children {
traverseFileTree(f, filePath, childTree)
}
}
// buildDirectoryTree creates a file tree, with the given directory as its root,
// representing the directory structure that exists relative to the given directory.
func buildDirectoryTree(basePath, directoryName string) (*FileTree, error) {
tree := &FileTree{
Name: directoryName,
Children: nil,
}
dirPath := filepath.Join(basePath, directoryName)
files, err := ioutil.ReadDir(dirPath)
if err != nil {
return nil, errors.Tracef("Failed to read directory %s with error: %s", dirPath, err.Error())
}
if len(files) > 0 {
for _, file := range files {
if file.IsDir() {
filePath := filepath.Join(basePath, directoryName)
childTree, err := buildDirectoryTree(filePath, file.Name())
if err != nil {
return nil, err
}
tree.Children = append(tree.Children, *childTree)
} else {
tree.Children = append(tree.Children, FileTree{
Name: file.Name(),
Children: nil,
})
}
}
}
return tree, nil
}