config_test.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. package psiphon
  20. import (
  21. "encoding/base64"
  22. "encoding/json"
  23. "io/ioutil"
  24. "strings"
  25. "testing"
  26. "github.com/stretchr/testify/suite"
  27. )
  28. const (
  29. _README = "../README.md"
  30. _README_CONFIG_BEGIN = "<!--BEGIN-SAMPLE-CONFIG-->"
  31. _README_CONFIG_END = "<!--END-SAMPLE-CONFIG-->"
  32. )
  33. type ConfigTestSuite struct {
  34. suite.Suite
  35. confStubBlob []byte
  36. requiredFields []string
  37. nonRequiredFields []string
  38. }
  39. func (suite *ConfigTestSuite) SetupSuite() {
  40. readmeBlob, _ := ioutil.ReadFile(_README)
  41. readmeString := string(readmeBlob)
  42. readmeString = readmeString[strings.Index(readmeString, _README_CONFIG_BEGIN)+len(_README_CONFIG_BEGIN) : strings.Index(readmeString, _README_CONFIG_END)]
  43. readmeString = strings.TrimSpace(readmeString)
  44. readmeString = strings.Trim(readmeString, "`")
  45. suite.confStubBlob = []byte(readmeString)
  46. var obj map[string]interface{}
  47. json.Unmarshal(suite.confStubBlob, &obj)
  48. for k, v := range obj {
  49. if v == "<placeholder>" {
  50. suite.requiredFields = append(suite.requiredFields, k)
  51. } else {
  52. suite.nonRequiredFields = append(suite.nonRequiredFields, k)
  53. }
  54. }
  55. }
  56. func TestConfigTestSuite(t *testing.T) {
  57. suite.Run(t, new(ConfigTestSuite))
  58. }
  59. // Tests good config
  60. func (suite *ConfigTestSuite) Test_LoadConfig_BasicGood() {
  61. _, err := LoadConfig(suite.confStubBlob)
  62. suite.Nil(err, "a basic config should succeed")
  63. }
  64. // Tests non-JSON file contents
  65. func (suite *ConfigTestSuite) Test_LoadConfig_BadFileContents() {
  66. _, err := LoadConfig([]byte(`this is not JSON`))
  67. suite.NotNil(err, "bytes that are not JSON at all should give an error")
  68. }
  69. // Tests config file with JSON contents that don't match our structure
  70. func (suite *ConfigTestSuite) Test_LoadConfig_BadJson() {
  71. var testObj map[string]interface{}
  72. var testObjJSON []byte
  73. // JSON with none of our fields
  74. _, err := LoadConfig([]byte(`{"f1": 11, "f2": "two"}`))
  75. suite.NotNil(err, "JSON with none of our fields should fail")
  76. // Test all required fields
  77. for _, field := range suite.requiredFields {
  78. // Missing a required field
  79. json.Unmarshal(suite.confStubBlob, &testObj)
  80. delete(testObj, field)
  81. testObjJSON, _ = json.Marshal(testObj)
  82. _, err = LoadConfig(testObjJSON)
  83. suite.NotNil(err, "JSON with one of our required fields missing should fail: %s", field)
  84. // Bad type for required field
  85. json.Unmarshal(suite.confStubBlob, &testObj)
  86. testObj[field] = false // basically guessing a wrong type
  87. testObjJSON, _ = json.Marshal(testObj)
  88. _, err = LoadConfig(testObjJSON)
  89. suite.NotNil(err, "JSON with one of our required fields with the wrong type should fail: %s", field)
  90. // One of our required fields is null
  91. json.Unmarshal(suite.confStubBlob, &testObj)
  92. testObj[field] = nil
  93. testObjJSON, _ = json.Marshal(testObj)
  94. _, err = LoadConfig(testObjJSON)
  95. suite.NotNil(err, "JSON with one of our required fields set to null should fail: %s", field)
  96. // One of our required fields is an empty string
  97. json.Unmarshal(suite.confStubBlob, &testObj)
  98. testObj[field] = ""
  99. testObjJSON, _ = json.Marshal(testObj)
  100. _, err = LoadConfig(testObjJSON)
  101. suite.NotNil(err, "JSON with one of our required fields set to an empty string should fail: %s", field)
  102. }
  103. // Test optional fields
  104. for _, field := range suite.nonRequiredFields {
  105. // Has incorrect type for optional field
  106. json.Unmarshal(suite.confStubBlob, &testObj)
  107. testObj[field] = false // basically guessing a wrong type
  108. testObjJSON, _ = json.Marshal(testObj)
  109. _, err = LoadConfig(testObjJSON)
  110. suite.NotNil(err, "JSON with one of our optional fields with the wrong type should fail: %s", field)
  111. }
  112. }
  113. // Tests config file with JSON contents that don't match our structure
  114. func (suite *ConfigTestSuite) Test_LoadConfig_GoodJson() {
  115. var testObj map[string]interface{}
  116. var testObjJSON []byte
  117. // TODO: Test that the config actually gets the values we expect?
  118. // Has all of our required fields, but no optional fields
  119. json.Unmarshal(suite.confStubBlob, &testObj)
  120. for i := range suite.nonRequiredFields {
  121. delete(testObj, suite.nonRequiredFields[i])
  122. }
  123. testObjJSON, _ = json.Marshal(testObj)
  124. _, err := LoadConfig(testObjJSON)
  125. suite.Nil(err, "JSON with good values for our required fields but no optional fields should succeed")
  126. // Has all of our required fields, and all optional fields
  127. _, err = LoadConfig(suite.confStubBlob)
  128. suite.Nil(err, "JSON with all good values for required and optional fields should succeed")
  129. // Has null for optional fields
  130. json.Unmarshal(suite.confStubBlob, &testObj)
  131. for i := range suite.nonRequiredFields {
  132. testObj[suite.nonRequiredFields[i]] = nil
  133. }
  134. testObjJSON, _ = json.Marshal(testObj)
  135. _, err = LoadConfig(testObjJSON)
  136. suite.Nil(err, "JSON with null for optional values should succeed")
  137. }
  138. func TestDownloadURLs(t *testing.T) {
  139. decodedA := "a.example.com"
  140. encodedA := base64.StdEncoding.EncodeToString([]byte(decodedA))
  141. encodedB := base64.StdEncoding.EncodeToString([]byte("b.example.com"))
  142. encodedC := base64.StdEncoding.EncodeToString([]byte("c.example.com"))
  143. testCases := []struct {
  144. description string
  145. downloadURLs []*DownloadURL
  146. attempts int
  147. expectedValid bool
  148. expectedCanonicalURL string
  149. expectedDistinctSelections int
  150. }{
  151. {
  152. "missing OnlyAfterAttempts = 0",
  153. []*DownloadURL{
  154. &DownloadURL{
  155. URL: encodedA,
  156. OnlyAfterAttempts: 1,
  157. },
  158. },
  159. 1,
  160. false,
  161. decodedA,
  162. 0,
  163. },
  164. {
  165. "single URL, multiple attempts",
  166. []*DownloadURL{
  167. &DownloadURL{
  168. URL: encodedA,
  169. OnlyAfterAttempts: 0,
  170. },
  171. },
  172. 2,
  173. true,
  174. decodedA,
  175. 1,
  176. },
  177. {
  178. "multiple URLs, single attempt",
  179. []*DownloadURL{
  180. &DownloadURL{
  181. URL: encodedA,
  182. OnlyAfterAttempts: 0,
  183. },
  184. &DownloadURL{
  185. URL: encodedB,
  186. OnlyAfterAttempts: 1,
  187. },
  188. &DownloadURL{
  189. URL: encodedC,
  190. OnlyAfterAttempts: 1,
  191. },
  192. },
  193. 1,
  194. true,
  195. decodedA,
  196. 1,
  197. },
  198. {
  199. "multiple URLs, multiple attempts",
  200. []*DownloadURL{
  201. &DownloadURL{
  202. URL: encodedA,
  203. OnlyAfterAttempts: 0,
  204. },
  205. &DownloadURL{
  206. URL: encodedB,
  207. OnlyAfterAttempts: 1,
  208. },
  209. &DownloadURL{
  210. URL: encodedC,
  211. OnlyAfterAttempts: 1,
  212. },
  213. },
  214. 2,
  215. true,
  216. decodedA,
  217. 3,
  218. },
  219. {
  220. "multiple URLs, multiple attempts",
  221. []*DownloadURL{
  222. &DownloadURL{
  223. URL: encodedA,
  224. OnlyAfterAttempts: 0,
  225. },
  226. &DownloadURL{
  227. URL: encodedB,
  228. OnlyAfterAttempts: 1,
  229. },
  230. &DownloadURL{
  231. URL: encodedC,
  232. OnlyAfterAttempts: 3,
  233. },
  234. },
  235. 4,
  236. true,
  237. decodedA,
  238. 3,
  239. },
  240. }
  241. for _, testCase := range testCases {
  242. t.Run(testCase.description, func(t *testing.T) {
  243. err := decodeAndValidateDownloadURLs(
  244. testCase.description,
  245. testCase.downloadURLs)
  246. if testCase.expectedValid {
  247. if err != nil {
  248. t.Fatalf("unexpected validation error: %s", err)
  249. }
  250. } else {
  251. if err == nil {
  252. t.Fatalf("expected validation error")
  253. }
  254. return
  255. }
  256. // Track distinct selections for each attempt; the
  257. // expected number of distinct should be for at least
  258. // one particular attempt.
  259. attemptDistinctSelections := make(map[int]map[string]int)
  260. for i := 0; i < testCase.attempts; i++ {
  261. attemptDistinctSelections[i] = make(map[string]int)
  262. }
  263. // Perform enough runs to account for random selection.
  264. runs := 1000
  265. attempt := 0
  266. for i := 0; i < runs; i++ {
  267. url, canonicalURL, skipVerify := selectDownloadURL(attempt, testCase.downloadURLs)
  268. if canonicalURL != testCase.expectedCanonicalURL {
  269. t.Fatalf("unexpected canonical URL: %s", canonicalURL)
  270. }
  271. if skipVerify {
  272. t.Fatalf("expected skipVerify")
  273. }
  274. attemptDistinctSelections[attempt][url] += 1
  275. attempt = (attempt + 1) % testCase.attempts
  276. }
  277. maxDistinctSelections := 0
  278. for _, m := range attemptDistinctSelections {
  279. if len(m) > maxDistinctSelections {
  280. maxDistinctSelections = len(m)
  281. }
  282. }
  283. if maxDistinctSelections != testCase.expectedDistinctSelections {
  284. t.Fatalf("got %d distinct selections, expected %d",
  285. maxDistinctSelections,
  286. testCase.expectedDistinctSelections)
  287. }
  288. })
  289. }
  290. }