domain_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. package store
  2. import (
  3. "bytes"
  4. "context"
  5. "database/sql"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "path/filepath"
  10. "strings"
  11. "testing"
  12. "time"
  13. )
  14. func TestMigrationFromAuthenticationSchema(t *testing.T) {
  15. ctx := context.Background()
  16. path := filepath.Join(t.TempDir(), "migration.db")
  17. raw, err := sql.Open("sqlite", path)
  18. if err != nil {
  19. t.Fatal(err)
  20. }
  21. for _, statement := range migrationStatements(1) {
  22. if _, err := raw.ExecContext(ctx, statement); err != nil {
  23. t.Fatalf("create v1 schema: %v", err)
  24. }
  25. }
  26. if _, err := raw.ExecContext(ctx, `
  27. INSERT INTO admins (id, username, password_hash, created_at, updated_at)
  28. VALUES (1, 'legacy-admin', X'0102', 100, 100)
  29. `); err != nil {
  30. t.Fatal(err)
  31. }
  32. if _, err := raw.ExecContext(ctx, `PRAGMA user_version = 1`); err != nil {
  33. t.Fatal(err)
  34. }
  35. if err := raw.Close(); err != nil {
  36. t.Fatal(err)
  37. }
  38. database := openTestStore(t, path)
  39. admin, err := database.CurrentAdmin(ctx)
  40. if err != nil {
  41. t.Fatalf("legacy admin missing after migration: %v", err)
  42. }
  43. if admin.Username != "legacy-admin" || !bytes.Equal(admin.PasswordHash, []byte{1, 2}) {
  44. t.Fatalf("legacy admin changed during migration: %+v", admin)
  45. }
  46. var version int
  47. if err := database.db.QueryRowContext(ctx, `PRAGMA user_version`).Scan(&version); err != nil {
  48. t.Fatal(err)
  49. }
  50. if version != schemaVersion {
  51. t.Fatalf("schema version = %d, want %d", version, schemaVersion)
  52. }
  53. for _, table := range []string{
  54. "devices", "device_runtime", "vowifi_runtime", "sms_messages",
  55. "local_proxy_config", "upstream_proxies", "country_rules",
  56. "device_proxy_bindings",
  57. "notification_settings", "app_settings", "audit_events",
  58. "log_events", "card_policies", "traffic_buckets",
  59. } {
  60. var found string
  61. err := database.db.QueryRowContext(ctx, `
  62. SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?
  63. `, table).Scan(&found)
  64. if err != nil || found != table {
  65. t.Fatalf("migrated table %q missing: %v", table, err)
  66. }
  67. }
  68. }
  69. func TestMigration4PreservesIMSRedeliveryAndUsesReceiptTime(t *testing.T) {
  70. ctx := context.Background()
  71. path := filepath.Join(t.TempDir(), "ims-redelivery.db")
  72. legacy := openTestStore(t, path)
  73. mustSaveDevice(t, legacy, "ec20-1", "EC20")
  74. smscTime := time.Unix(1_700_000_000, 0).UTC()
  75. firstReceipt := smscTime.Add(2 * time.Hour)
  76. rawTPDU := "040ED0D637396C7EBBCB000062808051715140"
  77. for index, receivedAt := range []time.Time{firstReceipt, firstReceipt.Add(30 * time.Minute)} {
  78. extra, err := json.Marshal(map[string]any{"raw_tpdu": rawTPDU, "call_id": index})
  79. if err != nil {
  80. t.Fatal(err)
  81. }
  82. if _, err := legacy.SaveSMSMessage(ctx, SMSMessage{
  83. MessageID: fmt.Sprintf("legacy-call-%d", index),
  84. DeviceID: "ec20-1",
  85. Peer: "Vodafone",
  86. Direction: "inbound",
  87. Body: "same message",
  88. Timestamp: smscTime,
  89. Status: "received",
  90. Source: "ims",
  91. CreatedAt: receivedAt,
  92. Extra: extra,
  93. }); err != nil {
  94. t.Fatal(err)
  95. }
  96. }
  97. if _, err := legacy.db.ExecContext(ctx, `PRAGMA user_version = 3`); err != nil {
  98. t.Fatal(err)
  99. }
  100. if err := legacy.Close(); err != nil {
  101. t.Fatal(err)
  102. }
  103. migrated := openTestStore(t, path)
  104. messages, err := migrated.ListSMSMessages(ctx, SMSFilter{DeviceID: "ec20-1"})
  105. if err != nil {
  106. t.Fatal(err)
  107. }
  108. if len(messages) != 2 {
  109. t.Fatalf("message count after migration = %d, want 2", len(messages))
  110. }
  111. if !messages[0].Timestamp.Equal(firstReceipt.Add(30*time.Minute)) ||
  112. !messages[1].Timestamp.Equal(firstReceipt) {
  113. t.Fatalf("message times = %v / %v, want both receipt times", messages[0].Timestamp, messages[1].Timestamp)
  114. }
  115. var extra map[string]any
  116. if err := json.Unmarshal(messages[0].Extra, &extra); err != nil {
  117. t.Fatal(err)
  118. }
  119. if extra["service_center_timestamp_unix"] != float64(smscTime.Unix()) {
  120. t.Fatalf("service center time was not retained: %#v", extra)
  121. }
  122. }
  123. func TestDeviceStateRoundTripAndCascade(t *testing.T) {
  124. ctx := context.Background()
  125. database := openTestStore(t, ":memory:")
  126. rsrp, rsrq, sinr := -95, -12, 15
  127. attached, inserted := true, true
  128. mode := 1
  129. device := Device{
  130. ID: "ec20-1",
  131. Name: "EC20 一号",
  132. Interface: "wwan0",
  133. ControlDevice: "/dev/cdc-wdm0",
  134. ATPort: "/dev/ttyUSB2",
  135. APN: "ims",
  136. ProxyPort: 1080,
  137. QMIUseProxy: true,
  138. NetworkEnabled: true,
  139. SMSEnabled: true,
  140. VoWiFiEnabled: true,
  141. Extra: json.RawMessage(`{"slot":1}`),
  142. }
  143. runtime := DeviceRuntime{
  144. Running: true,
  145. Healthy: true,
  146. ControlOnline: true,
  147. NetworkConnected: true,
  148. Operator: "China Mobile",
  149. SignalDBM: -71,
  150. SignalRSRP: &rsrp,
  151. SignalRSRQ: &rsrq,
  152. SignalSINR: &sinr,
  153. ICCID: "8986000000000000000",
  154. IMSI: "460001234567890",
  155. PSAttached: &attached,
  156. SIMInserted: &inserted,
  157. OperatingMode: &mode,
  158. PhoneNumber: "+8613800138000",
  159. PhoneNumberSource: "cnum",
  160. Traffic: json.RawMessage(`{"rx":"1 MiB"}`),
  161. }
  162. vowifi := VoWiFiRuntime{
  163. Phase: "sms_ready",
  164. SIMReady: true,
  165. AccessReady: true,
  166. TunnelReady: true,
  167. IMSReady: true,
  168. SMSReady: true,
  169. LocalPhone: "+8613800138000",
  170. PhoneNumberSource: "ims",
  171. Tunnel: json.RawMessage(`{"ifname":"ipsec0"}`),
  172. }
  173. if err := database.SaveDeviceState(ctx, device, &runtime, &vowifi); err != nil {
  174. t.Fatalf("SaveDeviceState() error = %v", err)
  175. }
  176. gotDevice, err := database.Device(ctx, device.ID)
  177. if err != nil {
  178. t.Fatal(err)
  179. }
  180. if gotDevice.BaudRate != 115200 || gotDevice.DataBits != 8 ||
  181. gotDevice.StopBits != 1 || gotDevice.DeviceBackend != "at" {
  182. t.Fatalf("device defaults not applied: %+v", gotDevice)
  183. }
  184. gotRuntime, err := database.DeviceRuntime(ctx, device.ID)
  185. if err != nil {
  186. t.Fatal(err)
  187. }
  188. if gotRuntime.PhoneNumber != runtime.PhoneNumber ||
  189. gotRuntime.SignalRSRP == nil || *gotRuntime.SignalRSRP != rsrp ||
  190. gotRuntime.PSAttached == nil || !*gotRuntime.PSAttached {
  191. t.Fatalf("runtime did not round trip: %+v", gotRuntime)
  192. }
  193. gotVoWiFi, err := database.VoWiFiRuntime(ctx, device.ID)
  194. if err != nil {
  195. t.Fatal(err)
  196. }
  197. if !gotVoWiFi.SMSReady || gotVoWiFi.LocalPhone != vowifi.LocalPhone {
  198. t.Fatalf("VoWiFi runtime did not round trip: %+v", gotVoWiFi)
  199. }
  200. if err := database.DeleteDevice(ctx, device.ID); err != nil {
  201. t.Fatal(err)
  202. }
  203. if _, err := database.DeviceRuntime(ctx, device.ID); !errors.Is(err, ErrNotFound) {
  204. t.Fatalf("runtime should cascade on device deletion, got %v", err)
  205. }
  206. if _, err := database.VoWiFiRuntime(ctx, device.ID); !errors.Is(err, ErrNotFound) {
  207. t.Fatalf("VoWiFi runtime should cascade on device deletion, got %v", err)
  208. }
  209. }
  210. func TestSMSPersistenceAndDerivedThreads(t *testing.T) {
  211. ctx := context.Background()
  212. database := openTestStore(t, ":memory:")
  213. mustSaveDevice(t, database, "ec20-1", "客厅")
  214. if err := database.UpsertDeviceRuntime(ctx, DeviceRuntime{
  215. DeviceID: "ec20-1",
  216. PhoneNumber: "+8613800138000",
  217. }); err != nil {
  218. t.Fatal(err)
  219. }
  220. base := time.Unix(1_700_000_000, 0).UTC()
  221. if err := database.SaveSMSMessages(ctx, []SMSMessage{
  222. {
  223. MessageID: "network-1", DeviceID: "ec20-1", IMSI: "46000",
  224. Peer: "10086", Direction: "inbound", Body: "第一条",
  225. Timestamp: base, Status: "received",
  226. },
  227. {
  228. MessageID: "network-2", DeviceID: "ec20-1", IMSI: "46000",
  229. Peer: "10086", Direction: "outbound", Body: "第二条",
  230. Timestamp: base.Add(time.Minute), Status: "sent", Read: true,
  231. },
  232. {
  233. MessageID: "network-3", DeviceID: "ec20-1", IMSI: "46000",
  234. Peer: "95533", Direction: "received", Body: "银行提醒",
  235. Timestamp: base.Add(2 * time.Minute), Status: "received",
  236. },
  237. }); err != nil {
  238. t.Fatalf("SaveSMSMessages() error = %v", err)
  239. }
  240. // A modem retry updates the stable external id instead of duplicating it.
  241. if _, err := database.SaveSMSMessage(ctx, SMSMessage{
  242. MessageID: "network-1", DeviceID: "ec20-1", IMSI: "46000",
  243. Peer: "10086", Direction: "inbound", Body: "第一条(完整)",
  244. Timestamp: base, Status: "received",
  245. }); err != nil {
  246. t.Fatal(err)
  247. }
  248. messages, err := database.ListSMSMessages(ctx, SMSFilter{DeviceID: "ec20-1"})
  249. if err != nil {
  250. t.Fatal(err)
  251. }
  252. if len(messages) != 3 {
  253. t.Fatalf("message count = %d, want 3", len(messages))
  254. }
  255. if !messages[2].Timestamp.Equal(base) {
  256. t.Fatalf("retry changed the original message time to %v", messages[2].Timestamp)
  257. }
  258. contacts, err := database.ListSMSContacts(ctx, SMSFilter{DeviceID: "ec20-1"})
  259. if err != nil {
  260. t.Fatal(err)
  261. }
  262. if len(contacts) != 2 || contacts[0].Peer != "95533" ||
  263. contacts[0].UnreadCount != 1 || contacts[1].Peer != "10086" ||
  264. contacts[1].MessageCount != 2 || contacts[1].UnreadCount != 1 ||
  265. contacts[1].LocalPhone != "+8613800138000" {
  266. t.Fatalf("unexpected derived contacts: %+v", contacts)
  267. }
  268. marked, err := database.MarkSMSThreadRead(ctx, "ec20-1", "46000", "10086")
  269. if err != nil || marked != 1 {
  270. t.Fatalf("MarkSMSThreadRead() = %d, %v", marked, err)
  271. }
  272. contacts, err = database.ListSMSContacts(ctx, SMSFilter{Peer: "10086"})
  273. if err != nil {
  274. t.Fatal(err)
  275. }
  276. if len(contacts) != 1 || contacts[0].UnreadCount != 0 {
  277. t.Fatalf("thread should be read: %+v", contacts)
  278. }
  279. deleted, err := database.DeleteSMSThread(ctx, "ec20-1", "46000", "10086")
  280. if err != nil || deleted != 2 {
  281. t.Fatalf("DeleteSMSThread() = %d, %v", deleted, err)
  282. }
  283. }
  284. func TestListInboundSMSAfterIDUsesDurableInsertionCursor(t *testing.T) {
  285. ctx := context.Background()
  286. database := openTestStore(t, ":memory:")
  287. mustSaveDevice(t, database, "ec20-1", "EC20")
  288. old, err := database.SaveSMSMessage(ctx, SMSMessage{
  289. MessageID: "old-inbound", DeviceID: "ec20-1", Peer: "10086",
  290. Direction: "inbound", Body: "old", Status: "received",
  291. })
  292. if err != nil {
  293. t.Fatal(err)
  294. }
  295. if _, err := database.SaveSMSMessage(ctx, SMSMessage{
  296. MessageID: "new-outbound", DeviceID: "ec20-1", Peer: "10010",
  297. Direction: "outbound", Body: "sent", Status: "sent",
  298. }); err != nil {
  299. t.Fatal(err)
  300. }
  301. newInbound, err := database.SaveSMSMessage(ctx, SMSMessage{
  302. MessageID: "new-inbound", DeviceID: "ec20-1", Peer: "95533",
  303. Direction: "received", Body: "new", Status: "received",
  304. })
  305. if err != nil {
  306. t.Fatal(err)
  307. }
  308. latest, err := database.LatestSMSMessageID(ctx)
  309. if err != nil || latest != newInbound.ID {
  310. t.Fatalf("LatestSMSMessageID() = %d, %v; want %d", latest, err, newInbound.ID)
  311. }
  312. messages, err := database.ListInboundSMSAfterID(ctx, old.ID, 100)
  313. if err != nil {
  314. t.Fatal(err)
  315. }
  316. if len(messages) != 1 || messages[0].ID != newInbound.ID {
  317. t.Fatalf("ListInboundSMSAfterID() = %#v", messages)
  318. }
  319. }
  320. func TestApplySMSDeliveryReportTracksEverySubmittedPart(t *testing.T) {
  321. ctx := context.Background()
  322. database := openTestStore(t, ":memory:")
  323. mustSaveDevice(t, database, "ec20-1", "EC20")
  324. extra := json.RawMessage(`{
  325. "transport":"ims",
  326. "part_results":[{"reference":42},{"reference":43}]
  327. }`)
  328. sent, err := database.SaveSMSMessage(ctx, SMSMessage{
  329. MessageID: "ims-submit-1", DeviceID: "ec20-1", IMSI: "23415",
  330. Peer: "+447700900123", Direction: "outbound", Body: "multipart",
  331. Timestamp: time.Now().UTC(), Status: "accepted_by_ims", Source: "ims",
  332. PartsTotal: 2, DeliveryState: "accepted_by_ims", Read: true, Extra: extra,
  333. })
  334. if err != nil {
  335. t.Fatal(err)
  336. }
  337. first, err := database.ApplySMSDeliveryReport(ctx, SMSDeliveryReport{
  338. DeviceID: "ec20-1", IMSI: "23415", Peer: "+447700900123", Source: "ims",
  339. MessageReference: 42, StatusCode: 0, DeliveryState: "delivered",
  340. })
  341. if err != nil || first.ID != sent.ID || first.DeliveryState != "pending_delivery_report" {
  342. t.Fatalf("first delivery report = (%#v, %v)", first, err)
  343. }
  344. second, err := database.ApplySMSDeliveryReport(ctx, SMSDeliveryReport{
  345. DeviceID: "ec20-1", IMSI: "23415", Peer: "+447700900123", Source: "ims",
  346. MessageReference: 43, StatusCode: 0, DeliveryState: "delivered",
  347. })
  348. if err != nil || second.ID != sent.ID || second.DeliveryState != "delivered" {
  349. t.Fatalf("second delivery report = (%#v, %v)", second, err)
  350. }
  351. var savedExtra map[string]any
  352. if err := json.Unmarshal(second.Extra, &savedExtra); err != nil {
  353. t.Fatal(err)
  354. }
  355. reports, _ := savedExtra["delivery_reports"].(map[string]any)
  356. if len(reports) != 2 {
  357. t.Fatalf("delivery reports = %#v", reports)
  358. }
  359. }
  360. func TestProxyCredentialsAndCountryRules(t *testing.T) {
  361. ctx := context.Background()
  362. database := openTestStore(t, ":memory:")
  363. mustSaveDevice(t, database, "ec20-1", "EC20")
  364. if err := database.UpsertLocalProxy(ctx, LocalProxyConfig{
  365. ID: "local-1", Name: "SOCKS", Mode: "socks5", DeviceID: "ec20-1",
  366. ListenAddr: "127.0.0.1", ListenPort: 1080, Enabled: true,
  367. AuthEnabled: true, Username: "user", Password: "local-secret",
  368. }); err != nil {
  369. t.Fatal(err)
  370. }
  371. if err := database.UpsertLocalProxy(ctx, LocalProxyConfig{
  372. ID: "local-1", Name: "SOCKS 新", Mode: "socks5", DeviceID: "ec20-1",
  373. ListenAddr: "127.0.0.1", ListenPort: 1080, Enabled: true,
  374. AuthEnabled: true, Username: "user", Password: "",
  375. }); err != nil {
  376. t.Fatal(err)
  377. }
  378. local, err := database.LocalProxy(ctx, "local-1")
  379. if err != nil {
  380. t.Fatal(err)
  381. }
  382. if local.Password != "local-secret" || local.Redacted().Password != SecretMask ||
  383. local.Public().Password != "" {
  384. t.Fatalf("local proxy credential semantics failed: %+v", local)
  385. }
  386. if err := database.UpsertUpstreamProxy(ctx, UpstreamProxy{
  387. ID: "up-1", Name: "上游", Addr: "127.0.0.1:2080",
  388. Username: "up-user", Password: "up-secret", Enabled: true,
  389. }); err != nil {
  390. t.Fatal(err)
  391. }
  392. if err := database.UpsertUpstreamProxy(ctx, UpstreamProxy{
  393. ID: "up-1", Name: "上游新", Addr: "127.0.0.1:2080",
  394. Username: "up-user", Password: SecretMask, Enabled: true,
  395. }); err != nil {
  396. t.Fatal(err)
  397. }
  398. upstream, err := database.UpstreamProxy(ctx, "up-1")
  399. if err != nil {
  400. t.Fatal(err)
  401. }
  402. if upstream.Password != "up-secret" {
  403. t.Fatalf("blank/masked update erased upstream secret: %+v", upstream)
  404. }
  405. if got := RedactText(
  406. "connect local-secret through up-secret",
  407. local,
  408. upstream,
  409. ); strings.Contains(got, "secret") {
  410. t.Fatalf("RedactText leaked credentials: %q", got)
  411. }
  412. if err := database.UpsertCountryRule(ctx, CountryRule{
  413. CountryCode: "cn", CountryName: "中国", UpstreamProxyID: "up-1",
  414. Enabled: true,
  415. }); err != nil {
  416. t.Fatal(err)
  417. }
  418. rule, err := database.CountryRule(ctx, "CN")
  419. if err != nil || rule.CountryCode != "CN" {
  420. t.Fatalf("CountryRule() = %+v, %v", rule, err)
  421. }
  422. if err := database.UpsertDeviceProxyBinding(ctx, DeviceProxyBinding{
  423. DeviceID: "ec20-1", UpstreamProxyID: "up-1",
  424. }); err != nil {
  425. t.Fatal(err)
  426. }
  427. binding, err := database.DeviceProxyBinding(ctx, "ec20-1")
  428. if err != nil || binding.UpstreamProxyID != "up-1" {
  429. t.Fatalf("DeviceProxyBinding() = %+v, %v", binding, err)
  430. }
  431. if err := database.DeleteUpstreamProxy(ctx, "up-1"); err != nil {
  432. t.Fatal(err)
  433. }
  434. if _, err := database.CountryRule(ctx, "CN"); !errors.Is(err, ErrNotFound) {
  435. t.Fatalf("country rule should cascade with upstream deletion, got %v", err)
  436. }
  437. if _, err := database.DeviceProxyBinding(ctx, "ec20-1"); !errors.Is(err, ErrNotFound) {
  438. t.Fatalf("device binding should cascade with upstream deletion, got %v", err)
  439. }
  440. }
  441. func TestNotificationAndAppSecretPreservation(t *testing.T) {
  442. ctx := context.Background()
  443. database := openTestStore(t, ":memory:")
  444. if err := database.SaveNotificationSettings(ctx, []NotificationSetting{
  445. {
  446. Channel: "email",
  447. Config: json.RawMessage(`{"password":"mail-secret"}`),
  448. },
  449. {
  450. Channel: "webhook",
  451. Config: json.RawMessage(`not-json`),
  452. },
  453. }); err == nil {
  454. t.Fatal("invalid notification batch was accepted")
  455. }
  456. if _, err := database.NotificationSetting(ctx, "email"); !errors.Is(err, ErrNotFound) {
  457. t.Fatalf("notification batch was not rolled back: %v", err)
  458. }
  459. if err := database.UpsertNotificationSetting(ctx, NotificationSetting{
  460. Channel: "telegram",
  461. Enabled: true,
  462. Config: json.RawMessage(`{"bot_token":"telegram-secret","chat_id":"1"}`),
  463. }); err != nil {
  464. t.Fatal(err)
  465. }
  466. if err := database.UpsertNotificationSetting(ctx, NotificationSetting{
  467. Channel: "telegram",
  468. Enabled: true,
  469. Config: json.RawMessage(`{"bot_token":"","chat_id":"2"}`),
  470. }); err != nil {
  471. t.Fatal(err)
  472. }
  473. setting, err := database.NotificationSetting(ctx, "telegram")
  474. if err != nil {
  475. t.Fatal(err)
  476. }
  477. var config map[string]any
  478. if err := json.Unmarshal(setting.Config, &config); err != nil {
  479. t.Fatal(err)
  480. }
  481. if config["bot_token"] != "telegram-secret" || config["chat_id"] != "2" {
  482. t.Fatalf("notification merge lost data: %s", setting.Config)
  483. }
  484. if bytes.Contains(setting.Redacted().Config, []byte("telegram-secret")) ||
  485. bytes.Contains(setting.Public().Config, []byte("telegram-secret")) {
  486. t.Fatal("notification views leaked secret")
  487. }
  488. if got := RedactText("token=telegram-secret", setting); strings.Contains(got, "telegram-secret") {
  489. t.Fatalf("notification secret leaked in text: %q", got)
  490. }
  491. if err := database.UpsertAppSetting(ctx, AppSetting{
  492. Key: "provider.token", Value: json.RawMessage(`"app-secret"`), Sensitive: true,
  493. }); err != nil {
  494. t.Fatal(err)
  495. }
  496. if err := database.UpsertAppSetting(ctx, AppSetting{
  497. Key: "provider.token", Value: json.RawMessage(`"********"`), Sensitive: true,
  498. }); err != nil {
  499. t.Fatal(err)
  500. }
  501. appSetting, err := database.AppSetting(ctx, "provider.token")
  502. if err != nil {
  503. t.Fatal(err)
  504. }
  505. if string(appSetting.Value) != `"app-secret"` ||
  506. string(appSetting.Redacted().Value) != `"********"` ||
  507. string(appSetting.Public().Value) != `null` {
  508. t.Fatalf("unexpected sensitive app setting: %+v", appSetting)
  509. }
  510. }
  511. func TestEventsPoliciesAndTraffic(t *testing.T) {
  512. ctx := context.Background()
  513. database := openTestStore(t, ":memory:")
  514. old := time.Unix(1_700_000_000, 0).UTC()
  515. recent := old.Add(time.Hour)
  516. if _, err := database.AppendAuditEvent(ctx, AuditEvent{
  517. Actor: "admin", Action: "device.update", EntityType: "device",
  518. EntityID: "ec20-1", Outcome: "ok", CreatedAt: old,
  519. }); err != nil {
  520. t.Fatal(err)
  521. }
  522. if _, err := database.AppendAuditEvent(ctx, AuditEvent{
  523. Actor: "system", Action: "device.refresh", EntityType: "device",
  524. EntityID: "ec20-1", Outcome: "ok", CreatedAt: recent,
  525. }); err != nil {
  526. t.Fatal(err)
  527. }
  528. audits, err := database.ListAuditEvents(ctx, AuditFilter{Actor: "admin"})
  529. if err != nil || len(audits) != 1 || audits[0].Action != "device.update" {
  530. t.Fatalf("audit filter result = %+v, %v", audits, err)
  531. }
  532. if _, err := database.AppendLogEvent(ctx, LogEvent{
  533. Time: old, Level: "warn", Message: "old warning",
  534. Fields: json.RawMessage(`{"device":"ec20-1"}`),
  535. }); err != nil {
  536. t.Fatal(err)
  537. }
  538. if _, err := database.AppendLogEvent(ctx, LogEvent{
  539. Time: recent, Level: "info", Message: "ready",
  540. }); err != nil {
  541. t.Fatal(err)
  542. }
  543. logs, err := database.ListLogEvents(ctx, LogFilter{Level: "info"})
  544. if err != nil || len(logs) != 1 || logs[0].Message != "ready" {
  545. t.Fatalf("log filter result = %+v, %v", logs, err)
  546. }
  547. auditDeleted, logDeleted, err := database.PruneEvents(
  548. ctx,
  549. old.Add(time.Minute),
  550. old.Add(time.Minute),
  551. )
  552. if err != nil || auditDeleted != 1 || logDeleted != 1 {
  553. t.Fatalf("PruneEvents() = %d, %d, %v", auditDeleted, logDeleted, err)
  554. }
  555. if err := database.UpsertCardPolicy(ctx, CardPolicy{
  556. ICCID: "89860001", NetworkEnabled: true, VoWiFiEnabled: true,
  557. APN: "ims", IPVersion: "ipv4v6",
  558. }); err != nil {
  559. t.Fatal(err)
  560. }
  561. if err := database.UpsertCardPolicy(ctx, CardPolicy{
  562. ICCID: "invalid", VoWiFiEnabled: true, AirplaneEnabled: true,
  563. }); err == nil {
  564. t.Fatal("invalid mutually exclusive card policy was accepted")
  565. }
  566. policy, err := database.CardPolicy(ctx, "89860001")
  567. if err != nil || !policy.VoWiFiEnabled {
  568. t.Fatalf("CardPolicy() = %+v, %v", policy, err)
  569. }
  570. period := old.Truncate(time.Hour)
  571. if err := database.UpsertTrafficBucket(ctx, TrafficBucket{
  572. DeviceID: "ec20-1", Bucket: "hour", PeriodStart: period,
  573. RXBytes: 100, TXBytes: 25,
  574. }); err != nil {
  575. t.Fatal(err)
  576. }
  577. if err := database.AddTrafficBucket(ctx, TrafficBucket{
  578. DeviceID: "ec20-1", Bucket: "hour", PeriodStart: period,
  579. RXBytes: 5, TXBytes: 10,
  580. }); err != nil {
  581. t.Fatal(err)
  582. }
  583. buckets, err := database.ListTrafficBuckets(ctx, TrafficFilter{
  584. DeviceID: "ec20-1", Bucket: "hour",
  585. })
  586. if err != nil || len(buckets) != 1 ||
  587. buckets[0].RXBytes != 105 || buckets[0].TXBytes != 35 ||
  588. buckets[0].TotalBytes() != 140 {
  589. t.Fatalf("traffic buckets = %+v, %v", buckets, err)
  590. }
  591. }
  592. func openTestStore(t *testing.T, path string) *Store {
  593. t.Helper()
  594. database, err := Open(context.Background(), path)
  595. if err != nil {
  596. t.Fatalf("Open(%q) error = %v", path, err)
  597. }
  598. t.Cleanup(func() {
  599. if err := database.Close(); err != nil {
  600. t.Errorf("Close() error = %v", err)
  601. }
  602. })
  603. return database
  604. }
  605. func mustSaveDevice(t *testing.T, database *Store, id, name string) {
  606. t.Helper()
  607. if err := database.UpsertDevice(context.Background(), Device{
  608. ID: id, Name: name, SMSEnabled: true,
  609. }); err != nil {
  610. t.Fatalf("UpsertDevice() error = %v", err)
  611. }
  612. }