policy_reader_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package source
  4. import (
  5. "cmp"
  6. "testing"
  7. "time"
  8. "tailscale.com/util/must"
  9. "tailscale.com/util/syspolicy/setting"
  10. )
  11. func TestReaderLifecycle(t *testing.T) {
  12. tests := []struct {
  13. name string
  14. origin *setting.Origin
  15. definitions []*setting.Definition
  16. wantReads []TestExpectedReads
  17. initStrings []TestSetting[string]
  18. initUInt64s []TestSetting[uint64]
  19. initWant *setting.Snapshot
  20. addStrings []TestSetting[string]
  21. addStringLists []TestSetting[[]string]
  22. newWant *setting.Snapshot
  23. }{
  24. {
  25. name: "read-all-settings-once",
  26. origin: setting.NewNamedOrigin("Test", setting.DeviceScope),
  27. definitions: []*setting.Definition{
  28. setting.NewDefinition("StringValue", setting.DeviceSetting, setting.StringValue),
  29. setting.NewDefinition("IntegerValue", setting.DeviceSetting, setting.IntegerValue),
  30. setting.NewDefinition("BooleanValue", setting.DeviceSetting, setting.BooleanValue),
  31. setting.NewDefinition("StringListValue", setting.DeviceSetting, setting.StringListValue),
  32. setting.NewDefinition("DurationValue", setting.DeviceSetting, setting.DurationValue),
  33. setting.NewDefinition("PreferenceOptionValue", setting.DeviceSetting, setting.PreferenceOptionValue),
  34. setting.NewDefinition("VisibilityValue", setting.DeviceSetting, setting.VisibilityValue),
  35. },
  36. wantReads: []TestExpectedReads{
  37. {Key: "StringValue", Type: setting.StringValue, NumTimes: 1},
  38. {Key: "IntegerValue", Type: setting.IntegerValue, NumTimes: 1},
  39. {Key: "BooleanValue", Type: setting.BooleanValue, NumTimes: 1},
  40. {Key: "StringListValue", Type: setting.StringListValue, NumTimes: 1},
  41. {Key: "DurationValue", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
  42. {Key: "PreferenceOptionValue", Type: setting.StringValue, NumTimes: 1}, // and so are [setting.PreferenceOption]s
  43. {Key: "VisibilityValue", Type: setting.StringValue, NumTimes: 1}, // and [setting.Visibility]
  44. },
  45. initWant: setting.NewSnapshot(nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  46. },
  47. {
  48. name: "re-read-all-settings-when-the-policy-changes",
  49. origin: setting.NewNamedOrigin("Test", setting.DeviceScope),
  50. definitions: []*setting.Definition{
  51. setting.NewDefinition("StringValue", setting.DeviceSetting, setting.StringValue),
  52. setting.NewDefinition("IntegerValue", setting.DeviceSetting, setting.IntegerValue),
  53. setting.NewDefinition("BooleanValue", setting.DeviceSetting, setting.BooleanValue),
  54. setting.NewDefinition("StringListValue", setting.DeviceSetting, setting.StringListValue),
  55. setting.NewDefinition("DurationValue", setting.DeviceSetting, setting.DurationValue),
  56. setting.NewDefinition("PreferenceOptionValue", setting.DeviceSetting, setting.PreferenceOptionValue),
  57. setting.NewDefinition("VisibilityValue", setting.DeviceSetting, setting.VisibilityValue),
  58. },
  59. wantReads: []TestExpectedReads{
  60. {Key: "StringValue", Type: setting.StringValue, NumTimes: 1},
  61. {Key: "IntegerValue", Type: setting.IntegerValue, NumTimes: 1},
  62. {Key: "BooleanValue", Type: setting.BooleanValue, NumTimes: 1},
  63. {Key: "StringListValue", Type: setting.StringListValue, NumTimes: 1},
  64. {Key: "DurationValue", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
  65. {Key: "PreferenceOptionValue", Type: setting.StringValue, NumTimes: 1}, // and so are [setting.PreferenceOption]s
  66. {Key: "VisibilityValue", Type: setting.StringValue, NumTimes: 1}, // and [setting.Visibility]
  67. },
  68. initWant: setting.NewSnapshot(nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  69. addStrings: []TestSetting[string]{TestSettingOf("StringValue", "S1")},
  70. addStringLists: []TestSetting[[]string]{TestSettingOf("StringListValue", []string{"S1", "S2", "S3"})},
  71. newWant: setting.NewSnapshot(map[setting.Key]setting.RawItem{
  72. "StringValue": setting.RawItemWith("S1", nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  73. "StringListValue": setting.RawItemWith([]string{"S1", "S2", "S3"}, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  74. }, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  75. },
  76. {
  77. name: "read-settings-if-in-scope/device",
  78. origin: setting.NewNamedOrigin("Test", setting.DeviceScope),
  79. definitions: []*setting.Definition{
  80. setting.NewDefinition("DeviceSetting", setting.DeviceSetting, setting.StringValue),
  81. setting.NewDefinition("ProfileSetting", setting.ProfileSetting, setting.IntegerValue),
  82. setting.NewDefinition("UserSetting", setting.UserSetting, setting.BooleanValue),
  83. },
  84. wantReads: []TestExpectedReads{
  85. {Key: "DeviceSetting", Type: setting.StringValue, NumTimes: 1},
  86. {Key: "ProfileSetting", Type: setting.IntegerValue, NumTimes: 1},
  87. {Key: "UserSetting", Type: setting.BooleanValue, NumTimes: 1},
  88. },
  89. },
  90. {
  91. name: "read-settings-if-in-scope/profile",
  92. origin: setting.NewNamedOrigin("Test", setting.CurrentProfileScope),
  93. definitions: []*setting.Definition{
  94. setting.NewDefinition("DeviceSetting", setting.DeviceSetting, setting.StringValue),
  95. setting.NewDefinition("ProfileSetting", setting.ProfileSetting, setting.IntegerValue),
  96. setting.NewDefinition("UserSetting", setting.UserSetting, setting.BooleanValue),
  97. },
  98. wantReads: []TestExpectedReads{
  99. // Device settings cannot be configured at the profile scope and should not be read.
  100. {Key: "ProfileSetting", Type: setting.IntegerValue, NumTimes: 1},
  101. {Key: "UserSetting", Type: setting.BooleanValue, NumTimes: 1},
  102. },
  103. },
  104. {
  105. name: "read-settings-if-in-scope/user",
  106. origin: setting.NewNamedOrigin("Test", setting.CurrentUserScope),
  107. definitions: []*setting.Definition{
  108. setting.NewDefinition("DeviceSetting", setting.DeviceSetting, setting.StringValue),
  109. setting.NewDefinition("ProfileSetting", setting.ProfileSetting, setting.IntegerValue),
  110. setting.NewDefinition("UserSetting", setting.UserSetting, setting.BooleanValue),
  111. },
  112. wantReads: []TestExpectedReads{
  113. // Device and profile settings cannot be configured at the profile scope and should not be read.
  114. {Key: "UserSetting", Type: setting.BooleanValue, NumTimes: 1},
  115. },
  116. },
  117. {
  118. name: "read-stringy-settings",
  119. origin: setting.NewNamedOrigin("Test", setting.DeviceScope),
  120. definitions: []*setting.Definition{
  121. setting.NewDefinition("DurationValue", setting.DeviceSetting, setting.DurationValue),
  122. setting.NewDefinition("PreferenceOptionValue", setting.DeviceSetting, setting.PreferenceOptionValue),
  123. setting.NewDefinition("VisibilityValue", setting.DeviceSetting, setting.VisibilityValue),
  124. },
  125. wantReads: []TestExpectedReads{
  126. {Key: "DurationValue", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
  127. {Key: "PreferenceOptionValue", Type: setting.StringValue, NumTimes: 1}, // and so are [setting.PreferenceOption]s
  128. {Key: "VisibilityValue", Type: setting.StringValue, NumTimes: 1}, // and [setting.Visibility]
  129. },
  130. initStrings: []TestSetting[string]{
  131. TestSettingOf("DurationValue", "2h30m"),
  132. TestSettingOf("PreferenceOptionValue", "always"),
  133. TestSettingOf("VisibilityValue", "show"),
  134. },
  135. initWant: setting.NewSnapshot(map[setting.Key]setting.RawItem{
  136. "DurationValue": setting.RawItemWith(must.Get(time.ParseDuration("2h30m")), nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  137. "PreferenceOptionValue": setting.RawItemWith(setting.AlwaysByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  138. "VisibilityValue": setting.RawItemWith(setting.VisibleByPolicy, nil, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  139. }, setting.NewNamedOrigin("Test", setting.DeviceScope)),
  140. },
  141. {
  142. name: "read-erroneous-stringy-settings",
  143. origin: setting.NewNamedOrigin("Test", setting.CurrentUserScope),
  144. definitions: []*setting.Definition{
  145. setting.NewDefinition("DurationValue1", setting.UserSetting, setting.DurationValue),
  146. setting.NewDefinition("DurationValue2", setting.UserSetting, setting.DurationValue),
  147. setting.NewDefinition("PreferenceOptionValue", setting.UserSetting, setting.PreferenceOptionValue),
  148. setting.NewDefinition("VisibilityValue", setting.UserSetting, setting.VisibilityValue),
  149. },
  150. wantReads: []TestExpectedReads{
  151. {Key: "DurationValue1", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
  152. {Key: "DurationValue2", Type: setting.StringValue, NumTimes: 1}, // duration is string from the [Store]'s perspective
  153. {Key: "PreferenceOptionValue", Type: setting.StringValue, NumTimes: 1}, // and so are [setting.PreferenceOption]s
  154. {Key: "VisibilityValue", Type: setting.StringValue, NumTimes: 1}, // and [setting.Visibility]
  155. },
  156. initStrings: []TestSetting[string]{
  157. TestSettingOf("DurationValue1", "soon"),
  158. TestSettingWithError[string]("DurationValue2", setting.NewErrorText("bang!")),
  159. TestSettingOf("PreferenceOptionValue", "sometimes"),
  160. },
  161. initUInt64s: []TestSetting[uint64]{
  162. TestSettingOf[uint64]("VisibilityValue", 42), // type mismatch
  163. },
  164. initWant: setting.NewSnapshot(map[setting.Key]setting.RawItem{
  165. "DurationValue1": setting.RawItemWith(nil, setting.NewErrorText("time: invalid duration \"soon\""), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
  166. "DurationValue2": setting.RawItemWith(nil, setting.NewErrorText("bang!"), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
  167. "PreferenceOptionValue": setting.RawItemWith(setting.ShowChoiceByPolicy, nil, setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
  168. "VisibilityValue": setting.RawItemWith(setting.VisibleByPolicy, setting.NewErrorText("type mismatch in ReadString: got uint64"), setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
  169. }, setting.NewNamedOrigin("Test", setting.CurrentUserScope)),
  170. },
  171. }
  172. for _, tt := range tests {
  173. t.Run(tt.name, func(t *testing.T) {
  174. setting.SetDefinitionsForTest(t, tt.definitions...)
  175. store := NewTestStore(t)
  176. store.SetStrings(tt.initStrings...)
  177. store.SetUInt64s(tt.initUInt64s...)
  178. reader, err := newReader(store, tt.origin)
  179. if err != nil {
  180. t.Fatalf("newReader failed: %v", err)
  181. }
  182. if got := reader.GetSettings(); tt.initWant != nil && !got.Equal(tt.initWant) {
  183. t.Errorf("Settings do not match: got %v, want %v", got, tt.initWant)
  184. }
  185. if tt.wantReads != nil {
  186. store.ReadsMustEqual(tt.wantReads...)
  187. }
  188. // Should not result in new reads as there were no changes.
  189. N := 100
  190. for range N {
  191. reader.GetSettings()
  192. }
  193. if tt.wantReads != nil {
  194. store.ReadsMustEqual(tt.wantReads...)
  195. }
  196. store.ResetCounters()
  197. got, err := reader.ReadSettings()
  198. if err != nil {
  199. t.Fatalf("ReadSettings failed: %v", err)
  200. }
  201. if tt.initWant != nil && !got.Equal(tt.initWant) {
  202. t.Errorf("Settings do not match: got %v, want %v", got, tt.initWant)
  203. }
  204. if tt.wantReads != nil {
  205. store.ReadsMustEqual(tt.wantReads...)
  206. }
  207. store.ResetCounters()
  208. if len(tt.addStrings) != 0 || len(tt.addStringLists) != 0 {
  209. store.SetStrings(tt.addStrings...)
  210. store.SetStringLists(tt.addStringLists...)
  211. // As the settings have changed, GetSettings needs to re-read them.
  212. if got, want := reader.GetSettings(), cmp.Or(tt.newWant, tt.initWant); !got.Equal(want) {
  213. t.Errorf("New Settings do not match: got %v, want %v", got, want)
  214. }
  215. if tt.wantReads != nil {
  216. store.ReadsMustEqual(tt.wantReads...)
  217. }
  218. }
  219. select {
  220. case <-reader.Done():
  221. t.Fatalf("the reader is closed")
  222. default:
  223. }
  224. store.Close()
  225. <-reader.Done()
  226. })
  227. }
  228. }
  229. func TestReadingSession(t *testing.T) {
  230. setting.SetDefinitionsForTest(t, setting.NewDefinition("StringValue", setting.DeviceSetting, setting.StringValue))
  231. store := NewTestStore(t)
  232. origin := setting.NewOrigin(setting.DeviceScope)
  233. reader, err := newReader(store, origin)
  234. if err != nil {
  235. t.Fatalf("newReader failed: %v", err)
  236. }
  237. session, err := reader.OpenSession()
  238. if err != nil {
  239. t.Fatalf("failed to open a reading session: %v", err)
  240. }
  241. t.Cleanup(session.Close)
  242. if got, want := session.GetSettings(), setting.NewSnapshot(nil, origin); !got.Equal(want) {
  243. t.Errorf("Settings do not match: got %v, want %v", got, want)
  244. }
  245. select {
  246. case _, ok := <-session.PolicyChanged():
  247. if ok {
  248. t.Fatalf("the policy changed notification was sent prematurely")
  249. } else {
  250. t.Fatalf("the session was closed prematurely")
  251. }
  252. default:
  253. }
  254. store.SetStrings(TestSettingOf("StringValue", "S1"))
  255. _, ok := <-session.PolicyChanged()
  256. if !ok {
  257. t.Fatalf("the session was closed prematurely")
  258. }
  259. want := setting.NewSnapshot(map[setting.Key]setting.RawItem{
  260. "StringValue": setting.RawItemWith("S1", nil, origin),
  261. }, origin)
  262. if got := session.GetSettings(); !got.Equal(want) {
  263. t.Errorf("Settings do not match: got %v, want %v", got, want)
  264. }
  265. store.Close()
  266. if _, ok = <-session.PolicyChanged(); ok {
  267. t.Fatalf("the session must be closed")
  268. }
  269. }