/* * 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/base64" "encoding/json" "io/ioutil" "strings" "testing" "github.com/stretchr/testify/suite" ) const ( _README = "../README.md" _README_CONFIG_BEGIN = "" _README_CONFIG_END = "" ) type ConfigTestSuite struct { suite.Suite confStubBlob []byte requiredFields []string nonRequiredFields []string } func (suite *ConfigTestSuite) SetupSuite() { readmeBlob, _ := ioutil.ReadFile(_README) readmeString := string(readmeBlob) readmeString = readmeString[strings.Index(readmeString, _README_CONFIG_BEGIN)+len(_README_CONFIG_BEGIN) : strings.Index(readmeString, _README_CONFIG_END)] readmeString = strings.TrimSpace(readmeString) readmeString = strings.Trim(readmeString, "`") suite.confStubBlob = []byte(readmeString) var obj map[string]interface{} json.Unmarshal(suite.confStubBlob, &obj) for k, v := range obj { if v == "" { suite.requiredFields = append(suite.requiredFields, k) } else { suite.nonRequiredFields = append(suite.nonRequiredFields, k) } } } func TestConfigTestSuite(t *testing.T) { suite.Run(t, new(ConfigTestSuite)) } // Tests good config func (suite *ConfigTestSuite) Test_LoadConfig_BasicGood() { _, err := LoadConfig(suite.confStubBlob) 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 _, err := LoadConfig([]byte(`{"f1": 11, "f2": "two"}`)) 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) _, err = LoadConfig(testObjJSON) 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) _, err = LoadConfig(testObjJSON) 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) _, err = LoadConfig(testObjJSON) 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) _, err = LoadConfig(testObjJSON) 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) _, err = LoadConfig(testObjJSON) 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) _, err := LoadConfig(testObjJSON) 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 _, err = LoadConfig(suite.confStubBlob) 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) _, err = LoadConfig(testObjJSON) suite.Nil(err, "JSON with null for optional values should succeed") } func TestDownloadURLs(t *testing.T) { decodedA := "a.example.com" encodedA := base64.StdEncoding.EncodeToString([]byte(decodedA)) encodedB := base64.StdEncoding.EncodeToString([]byte("b.example.com")) encodedC := base64.StdEncoding.EncodeToString([]byte("c.example.com")) testCases := []struct { description string downloadURLs []*DownloadURL attempts int expectedValid bool expectedCanonicalURL string expectedDistinctSelections int }{ { "missing OnlyAfterAttempts = 0", []*DownloadURL{ { URL: encodedA, OnlyAfterAttempts: 1, }, }, 1, false, decodedA, 0, }, { "single URL, multiple attempts", []*DownloadURL{ { URL: encodedA, OnlyAfterAttempts: 0, }, }, 2, true, decodedA, 1, }, { "multiple URLs, single attempt", []*DownloadURL{ { URL: encodedA, OnlyAfterAttempts: 0, }, { URL: encodedB, OnlyAfterAttempts: 1, }, { URL: encodedC, OnlyAfterAttempts: 1, }, }, 1, true, decodedA, 1, }, { "multiple URLs, multiple attempts", []*DownloadURL{ { URL: encodedA, OnlyAfterAttempts: 0, }, { URL: encodedB, OnlyAfterAttempts: 1, }, { URL: encodedC, OnlyAfterAttempts: 1, }, }, 2, true, decodedA, 3, }, { "multiple URLs, multiple attempts", []*DownloadURL{ { URL: encodedA, OnlyAfterAttempts: 0, }, { URL: encodedB, OnlyAfterAttempts: 1, }, { URL: encodedC, OnlyAfterAttempts: 3, }, }, 4, true, decodedA, 3, }, } for _, testCase := range testCases { t.Run(testCase.description, func(t *testing.T) { err := decodeAndValidateDownloadURLs( testCase.description, testCase.downloadURLs) if testCase.expectedValid { if err != nil { t.Fatalf("unexpected validation error: %s", err) } } else { if err == nil { t.Fatalf("expected validation error") } return } // Track distinct selections for each attempt; the // expected number of distinct should be for at least // one particular attempt. attemptDistinctSelections := make(map[int]map[string]int) for i := 0; i < testCase.attempts; i++ { attemptDistinctSelections[i] = make(map[string]int) } // Perform enough runs to account for random selection. runs := 1000 attempt := 0 for i := 0; i < runs; i++ { url, canonicalURL, skipVerify := selectDownloadURL(attempt, testCase.downloadURLs) if canonicalURL != testCase.expectedCanonicalURL { t.Fatalf("unexpected canonical URL: %s", canonicalURL) } if skipVerify { t.Fatalf("expected skipVerify") } attemptDistinctSelections[attempt][url] += 1 attempt = (attempt + 1) % testCase.attempts } maxDistinctSelections := 0 for _, m := range attemptDistinctSelections { if len(m) > maxDistinctSelections { maxDistinctSelections = len(m) } } if maxDistinctSelections != testCase.expectedDistinctSelections { t.Fatalf("got %d distinct selections, expected %d", maxDistinctSelections, testCase.expectedDistinctSelections) } }) } }