clientupdate_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package clientupdate
  4. import (
  5. "archive/tar"
  6. "compress/gzip"
  7. "fmt"
  8. "io/fs"
  9. "maps"
  10. "os"
  11. "path/filepath"
  12. "slices"
  13. "sort"
  14. "strings"
  15. "testing"
  16. )
  17. func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
  18. tests := []struct {
  19. name string
  20. toTrack string
  21. in string
  22. want string // empty means want no change
  23. wantErr string
  24. }{
  25. {
  26. name: "stable-to-unstable",
  27. toTrack: UnstableTrack,
  28. in: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/stable/debian bullseye main\n",
  29. want: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
  30. },
  31. {
  32. name: "stable-unchanged",
  33. toTrack: StableTrack,
  34. in: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/stable/debian bullseye main\n",
  35. },
  36. {
  37. name: "if-both-stable-and-unstable-dont-change",
  38. toTrack: StableTrack,
  39. in: "# Tailscale packages for debian buster\n" +
  40. "deb https://pkgs.tailscale.com/stable/debian bullseye main\n" +
  41. "deb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
  42. },
  43. {
  44. name: "if-both-stable-and-unstable-dont-change-unstable",
  45. toTrack: UnstableTrack,
  46. in: "# Tailscale packages for debian buster\n" +
  47. "deb https://pkgs.tailscale.com/stable/debian bullseye main\n" +
  48. "deb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
  49. },
  50. {
  51. name: "signed-by-form",
  52. toTrack: UnstableTrack,
  53. in: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu jammy main\n",
  54. want: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/unstable/ubuntu jammy main\n",
  55. },
  56. {
  57. name: "unsupported-lines",
  58. toTrack: UnstableTrack,
  59. in: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/foobar/ubuntu jammy main\n",
  60. wantErr: "unexpected/unsupported /etc/apt/sources.list.d/tailscale.list contents",
  61. },
  62. }
  63. for _, tt := range tests {
  64. t.Run(tt.name, func(t *testing.T) {
  65. newContent, err := updateDebianAptSourcesListBytes([]byte(tt.in), tt.toTrack)
  66. if err != nil {
  67. if err.Error() != tt.wantErr {
  68. t.Fatalf("error = %v; want %q", err, tt.wantErr)
  69. }
  70. return
  71. }
  72. if tt.wantErr != "" {
  73. t.Fatalf("got no error; want %q", tt.wantErr)
  74. }
  75. var gotChange string
  76. if string(newContent) != tt.in {
  77. gotChange = string(newContent)
  78. }
  79. if gotChange != tt.want {
  80. t.Errorf("wrong result\n got: %q\nwant: %q", gotChange, tt.want)
  81. }
  82. })
  83. }
  84. }
  85. func TestUpdateYUMRepoTrack(t *testing.T) {
  86. tests := []struct {
  87. desc string
  88. before string
  89. track string
  90. after string
  91. rewrote bool
  92. wantErr bool
  93. }{
  94. {
  95. desc: "same track",
  96. before: `
  97. [tailscale-stable]
  98. name=Tailscale stable
  99. baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
  100. enabled=1
  101. type=rpm
  102. repo_gpgcheck=1
  103. gpgcheck=0
  104. gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
  105. `,
  106. track: StableTrack,
  107. after: `
  108. [tailscale-stable]
  109. name=Tailscale stable
  110. baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
  111. enabled=1
  112. type=rpm
  113. repo_gpgcheck=1
  114. gpgcheck=0
  115. gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
  116. `,
  117. },
  118. {
  119. desc: "change track",
  120. before: `
  121. [tailscale-stable]
  122. name=Tailscale stable
  123. baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
  124. enabled=1
  125. type=rpm
  126. repo_gpgcheck=1
  127. gpgcheck=0
  128. gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
  129. `,
  130. track: UnstableTrack,
  131. after: `
  132. [tailscale-unstable]
  133. name=Tailscale unstable
  134. baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
  135. enabled=1
  136. type=rpm
  137. repo_gpgcheck=1
  138. gpgcheck=0
  139. gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
  140. `,
  141. rewrote: true,
  142. },
  143. {
  144. desc: "non-tailscale repo file",
  145. before: `
  146. [fedora]
  147. name=Fedora $releasever - $basearch
  148. #baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/
  149. metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch
  150. enabled=1
  151. countme=1
  152. metadata_expire=7d
  153. repo_gpgcheck=0
  154. type=rpm
  155. gpgcheck=1
  156. gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
  157. skip_if_unavailable=False
  158. `,
  159. track: StableTrack,
  160. wantErr: true,
  161. },
  162. }
  163. for _, tt := range tests {
  164. t.Run(tt.desc, func(t *testing.T) {
  165. path := filepath.Join(t.TempDir(), "tailscale.repo")
  166. if err := os.WriteFile(path, []byte(tt.before), 0644); err != nil {
  167. t.Fatal(err)
  168. }
  169. rewrote, err := updateYUMRepoTrack(path, tt.track)
  170. if err == nil && tt.wantErr {
  171. t.Fatal("got nil error, want non-nil")
  172. }
  173. if err != nil && !tt.wantErr {
  174. t.Fatalf("got error %q, want nil", err)
  175. }
  176. if err != nil {
  177. return
  178. }
  179. if rewrote != tt.rewrote {
  180. t.Errorf("got rewrote flag %v, want %v", rewrote, tt.rewrote)
  181. }
  182. after, err := os.ReadFile(path)
  183. if err != nil {
  184. t.Fatal(err)
  185. }
  186. if string(after) != tt.after {
  187. t.Errorf("got repo file after update:\n%swant:\n%s", after, tt.after)
  188. }
  189. })
  190. }
  191. }
  192. func TestParseAlpinePackageVersion(t *testing.T) {
  193. tests := []struct {
  194. desc string
  195. out string
  196. want string
  197. wantErr bool
  198. }{
  199. {
  200. desc: "valid version",
  201. out: `
  202. tailscale-1.44.2-r0 description:
  203. The easiest, most secure way to use WireGuard and 2FA
  204. tailscale-1.44.2-r0 webpage:
  205. https://tailscale.com/
  206. tailscale-1.44.2-r0 installed size:
  207. 32 MiB
  208. `,
  209. want: "1.44.2",
  210. },
  211. {
  212. desc: "wrong package output",
  213. out: `
  214. busybox-1.36.1-r0 description:
  215. Size optimized toolbox of many common UNIX utilities
  216. busybox-1.36.1-r0 webpage:
  217. https://busybox.net/
  218. busybox-1.36.1-r0 installed size:
  219. 924 KiB
  220. `,
  221. wantErr: true,
  222. },
  223. {
  224. desc: "missing version",
  225. out: `
  226. tailscale description:
  227. The easiest, most secure way to use WireGuard and 2FA
  228. tailscale webpage:
  229. https://tailscale.com/
  230. tailscale installed size:
  231. 32 MiB
  232. `,
  233. wantErr: true,
  234. },
  235. {
  236. desc: "empty output",
  237. out: "",
  238. wantErr: true,
  239. },
  240. {
  241. desc: "multiple versions",
  242. out: `
  243. tailscale-1.54.1-r0 description:
  244. The easiest, most secure way to use WireGuard and 2FA
  245. tailscale-1.54.1-r0 webpage:
  246. https://tailscale.com/
  247. tailscale-1.54.1-r0 installed size:
  248. 34 MiB
  249. tailscale-1.58.2-r0 description:
  250. The easiest, most secure way to use WireGuard and 2FA
  251. tailscale-1.58.2-r0 webpage:
  252. https://tailscale.com/
  253. tailscale-1.58.2-r0 installed size:
  254. 35 MiB
  255. `,
  256. want: "1.58.2",
  257. },
  258. }
  259. for _, tt := range tests {
  260. t.Run(tt.desc, func(t *testing.T) {
  261. got, err := parseAlpinePackageVersion([]byte(tt.out))
  262. if err == nil && tt.wantErr {
  263. t.Fatalf("got nil error and version %q, want non-nil error", got)
  264. }
  265. if err != nil && !tt.wantErr {
  266. t.Fatalf("got error: %q, want nil", err)
  267. }
  268. if got != tt.want {
  269. t.Fatalf("got version: %q, want %q", got, tt.want)
  270. }
  271. })
  272. }
  273. }
  274. func TestSynoArch(t *testing.T) {
  275. tests := []struct {
  276. goarch string
  277. synoinfoUnique string
  278. want string
  279. wantErr bool
  280. }{
  281. {goarch: "amd64", synoinfoUnique: "synology_x86_224", want: "x86_64"},
  282. {goarch: "arm64", synoinfoUnique: "synology_armv8_124", want: "armv8"},
  283. {goarch: "386", synoinfoUnique: "synology_i686_415play", want: "i686"},
  284. {goarch: "arm", synoinfoUnique: "synology_88f6281_213air", want: "88f6281"},
  285. {goarch: "arm", synoinfoUnique: "synology_88f6282_413j", want: "88f6282"},
  286. {goarch: "arm", synoinfoUnique: "synology_hi3535_NVR1218", want: "hi3535"},
  287. {goarch: "arm", synoinfoUnique: "synology_alpine_1517", want: "alpine"},
  288. {goarch: "arm", synoinfoUnique: "synology_armada370_216se", want: "armada370"},
  289. {goarch: "arm", synoinfoUnique: "synology_armada375_115", want: "armada375"},
  290. {goarch: "arm", synoinfoUnique: "synology_armada38x_419slim", want: "armada38x"},
  291. {goarch: "arm", synoinfoUnique: "synology_armadaxp_RS815", want: "armadaxp"},
  292. {goarch: "arm", synoinfoUnique: "synology_comcerto2k_414j", want: "comcerto2k"},
  293. {goarch: "arm", synoinfoUnique: "synology_monaco_216play", want: "monaco"},
  294. {goarch: "ppc64", synoinfoUnique: "synology_qoriq_413", wantErr: true},
  295. }
  296. for _, tt := range tests {
  297. t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.synoinfoUnique), func(t *testing.T) {
  298. synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
  299. if err := os.WriteFile(
  300. synoinfoConfPath,
  301. []byte(fmt.Sprintf("unique=%q\n", tt.synoinfoUnique)),
  302. 0600,
  303. ); err != nil {
  304. t.Fatal(err)
  305. }
  306. got, err := synoArch(tt.goarch, synoinfoConfPath)
  307. if err != nil {
  308. if !tt.wantErr {
  309. t.Fatalf("got unexpected error %v", err)
  310. }
  311. return
  312. }
  313. if tt.wantErr {
  314. t.Fatalf("got %q, expected an error", got)
  315. }
  316. if got != tt.want {
  317. t.Errorf("got %q, want %q", got, tt.want)
  318. }
  319. })
  320. }
  321. }
  322. func TestParseSynoinfo(t *testing.T) {
  323. tests := []struct {
  324. desc string
  325. content string
  326. want string
  327. wantErr bool
  328. }{
  329. {
  330. desc: "double-quoted",
  331. content: `
  332. company_title="Synology"
  333. unique="synology_88f6281_213air"
  334. `,
  335. want: "88f6281",
  336. },
  337. {
  338. desc: "single-quoted",
  339. content: `
  340. company_title="Synology"
  341. unique='synology_88f6281_213air'
  342. `,
  343. want: "88f6281",
  344. },
  345. {
  346. desc: "unquoted",
  347. content: `
  348. company_title="Synology"
  349. unique=synology_88f6281_213air
  350. `,
  351. want: "88f6281",
  352. },
  353. {
  354. desc: "missing unique",
  355. content: `
  356. company_title="Synology"
  357. `,
  358. wantErr: true,
  359. },
  360. {
  361. desc: "empty unique",
  362. content: `
  363. company_title="Synology"
  364. unique=
  365. `,
  366. wantErr: true,
  367. },
  368. {
  369. desc: "empty unique double-quoted",
  370. content: `
  371. company_title="Synology"
  372. unique=""
  373. `,
  374. wantErr: true,
  375. },
  376. {
  377. desc: "empty unique single-quoted",
  378. content: `
  379. company_title="Synology"
  380. unique=''
  381. `,
  382. wantErr: true,
  383. },
  384. {
  385. desc: "malformed unique",
  386. content: `
  387. company_title="Synology"
  388. unique="synology_88f6281"
  389. `,
  390. wantErr: true,
  391. },
  392. {
  393. desc: "empty file",
  394. content: ``,
  395. wantErr: true,
  396. },
  397. {
  398. desc: "empty lines and comments",
  399. content: `
  400. # In a file named synoinfo? Shocking!
  401. company_title="Synology"
  402. # unique= is_a_field_that_follows
  403. unique="synology_88f6281_213air"
  404. `,
  405. want: "88f6281",
  406. },
  407. }
  408. for _, tt := range tests {
  409. t.Run(tt.desc, func(t *testing.T) {
  410. synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
  411. if err := os.WriteFile(synoinfoConfPath, []byte(tt.content), 0600); err != nil {
  412. t.Fatal(err)
  413. }
  414. got, err := parseSynoinfo(synoinfoConfPath)
  415. if err != nil {
  416. if !tt.wantErr {
  417. t.Fatalf("got unexpected error %v", err)
  418. }
  419. return
  420. }
  421. if tt.wantErr {
  422. t.Fatalf("got %q, expected an error", got)
  423. }
  424. if got != tt.want {
  425. t.Errorf("got %q, want %q", got, tt.want)
  426. }
  427. })
  428. }
  429. }
  430. func TestUnpackLinuxTarball(t *testing.T) {
  431. oldBinaryPaths := binaryPaths
  432. t.Cleanup(func() { binaryPaths = oldBinaryPaths })
  433. tests := []struct {
  434. desc string
  435. tarball map[string]string
  436. before map[string]string
  437. after map[string]string
  438. wantErr bool
  439. }{
  440. {
  441. desc: "success",
  442. before: map[string]string{
  443. "tailscale": "v1",
  444. "tailscaled": "v1",
  445. },
  446. tarball: map[string]string{
  447. "/usr/bin/tailscale": "v2",
  448. "/usr/bin/tailscaled": "v2",
  449. },
  450. after: map[string]string{
  451. "tailscale": "v2",
  452. "tailscaled": "v2",
  453. },
  454. },
  455. {
  456. desc: "don't touch unrelated files",
  457. before: map[string]string{
  458. "tailscale": "v1",
  459. "tailscaled": "v1",
  460. "foo": "bar",
  461. },
  462. tarball: map[string]string{
  463. "/usr/bin/tailscale": "v2",
  464. "/usr/bin/tailscaled": "v2",
  465. },
  466. after: map[string]string{
  467. "tailscale": "v2",
  468. "tailscaled": "v2",
  469. "foo": "bar",
  470. },
  471. },
  472. {
  473. desc: "unmodified",
  474. before: map[string]string{
  475. "tailscale": "v1",
  476. "tailscaled": "v1",
  477. },
  478. tarball: map[string]string{
  479. "/usr/bin/tailscale": "v1",
  480. "/usr/bin/tailscaled": "v1",
  481. },
  482. after: map[string]string{
  483. "tailscale": "v1",
  484. "tailscaled": "v1",
  485. },
  486. },
  487. {
  488. desc: "ignore extra tarball files",
  489. before: map[string]string{
  490. "tailscale": "v1",
  491. "tailscaled": "v1",
  492. },
  493. tarball: map[string]string{
  494. "/usr/bin/tailscale": "v2",
  495. "/usr/bin/tailscaled": "v2",
  496. "/systemd/tailscaled.service": "v2",
  497. },
  498. after: map[string]string{
  499. "tailscale": "v2",
  500. "tailscaled": "v2",
  501. },
  502. },
  503. {
  504. desc: "tarball missing tailscaled",
  505. before: map[string]string{
  506. "tailscale": "v1",
  507. "tailscaled": "v1",
  508. },
  509. tarball: map[string]string{
  510. "/usr/bin/tailscale": "v2",
  511. },
  512. after: map[string]string{
  513. "tailscale": "v1",
  514. "tailscale.new": "v2",
  515. "tailscaled": "v1",
  516. },
  517. wantErr: true,
  518. },
  519. {
  520. desc: "duplicate tailscale binary",
  521. before: map[string]string{
  522. "tailscale": "v1",
  523. "tailscaled": "v1",
  524. },
  525. tarball: map[string]string{
  526. "/usr/bin/tailscale": "v2",
  527. "/usr/sbin/tailscale": "v2",
  528. "/usr/bin/tailscaled": "v2",
  529. },
  530. after: map[string]string{
  531. "tailscale": "v1",
  532. "tailscale.new": "v2",
  533. "tailscaled": "v1",
  534. "tailscaled.new": "v2",
  535. },
  536. wantErr: true,
  537. },
  538. {
  539. desc: "empty archive",
  540. before: map[string]string{
  541. "tailscale": "v1",
  542. "tailscaled": "v1",
  543. },
  544. tarball: map[string]string{},
  545. after: map[string]string{
  546. "tailscale": "v1",
  547. "tailscaled": "v1",
  548. },
  549. wantErr: true,
  550. },
  551. }
  552. for _, tt := range tests {
  553. t.Run(tt.desc, func(t *testing.T) {
  554. // Swap out binaryPaths function to point at dummy file paths.
  555. tmp := t.TempDir()
  556. tailscalePath := filepath.Join(tmp, "tailscale")
  557. tailscaledPath := filepath.Join(tmp, "tailscaled")
  558. binaryPaths = func() (string, string, error) {
  559. return tailscalePath, tailscaledPath, nil
  560. }
  561. for name, content := range tt.before {
  562. if err := os.WriteFile(filepath.Join(tmp, name), []byte(content), 0755); err != nil {
  563. t.Fatal(err)
  564. }
  565. }
  566. tarPath := filepath.Join(tmp, "tailscale.tgz")
  567. genTarball(t, tarPath, tt.tarball)
  568. up := &Updater{Arguments: Arguments{Logf: t.Logf}}
  569. err := up.unpackLinuxTarball(tarPath)
  570. if err != nil {
  571. if !tt.wantErr {
  572. t.Fatalf("unexpected error: %v", err)
  573. }
  574. } else if tt.wantErr {
  575. t.Fatalf("unpack succeeded, expected an error")
  576. }
  577. gotAfter := make(map[string]string)
  578. err = filepath.WalkDir(tmp, func(path string, d fs.DirEntry, err error) error {
  579. if err != nil {
  580. return err
  581. }
  582. if d.Type().IsDir() {
  583. return nil
  584. }
  585. if path == tarPath {
  586. return nil
  587. }
  588. content, err := os.ReadFile(path)
  589. if err != nil {
  590. return err
  591. }
  592. path = filepath.ToSlash(path)
  593. base := filepath.ToSlash(tmp)
  594. gotAfter[strings.TrimPrefix(path, base+"/")] = string(content)
  595. return nil
  596. })
  597. if err != nil {
  598. t.Fatal(err)
  599. }
  600. if !maps.Equal(gotAfter, tt.after) {
  601. t.Errorf("files after unpack: %+v, want %+v", gotAfter, tt.after)
  602. }
  603. })
  604. }
  605. }
  606. func genTarball(t *testing.T, path string, files map[string]string) {
  607. f, err := os.Create(path)
  608. if err != nil {
  609. t.Fatal(err)
  610. }
  611. defer f.Close()
  612. gw := gzip.NewWriter(f)
  613. defer gw.Close()
  614. tw := tar.NewWriter(gw)
  615. defer tw.Close()
  616. for file, content := range files {
  617. if err := tw.WriteHeader(&tar.Header{
  618. Name: file,
  619. Size: int64(len(content)),
  620. Mode: 0755,
  621. }); err != nil {
  622. t.Fatal(err)
  623. }
  624. if _, err := tw.Write([]byte(content)); err != nil {
  625. t.Fatal(err)
  626. }
  627. }
  628. }
  629. func TestWriteFileOverwrite(t *testing.T) {
  630. path := filepath.Join(t.TempDir(), "test")
  631. for i := range 2 {
  632. content := fmt.Sprintf("content %d", i)
  633. if err := writeFile(strings.NewReader(content), path, 0600); err != nil {
  634. t.Fatal(err)
  635. }
  636. got, err := os.ReadFile(path)
  637. if err != nil {
  638. t.Fatal(err)
  639. }
  640. if string(got) != content {
  641. t.Errorf("got content: %q, want: %q", got, content)
  642. }
  643. }
  644. }
  645. func TestWriteFileSymlink(t *testing.T) {
  646. // Test for a malicious symlink at the destination path.
  647. // f2 points to f1 and writeFile(f2) should not end up overwriting f1.
  648. tmp := t.TempDir()
  649. f1 := filepath.Join(tmp, "f1")
  650. if err := os.WriteFile(f1, []byte("old"), 0600); err != nil {
  651. t.Fatal(err)
  652. }
  653. f2 := filepath.Join(tmp, "f2")
  654. if err := os.Symlink(f1, f2); err != nil {
  655. t.Fatal(err)
  656. }
  657. if err := writeFile(strings.NewReader("new"), f2, 0600); err != nil {
  658. t.Errorf("writeFile(%q) failed: %v", f2, err)
  659. }
  660. want := map[string]string{
  661. f1: "old",
  662. f2: "new",
  663. }
  664. for f, content := range want {
  665. got, err := os.ReadFile(f)
  666. if err != nil {
  667. t.Fatal(err)
  668. }
  669. if string(got) != content {
  670. t.Errorf("%q: got content %q, want %q", f, got, content)
  671. }
  672. }
  673. }
  674. func TestCleanupOldDownloads(t *testing.T) {
  675. tests := []struct {
  676. desc string
  677. before []string
  678. symlinks map[string]string
  679. glob string
  680. after []string
  681. }{
  682. {
  683. desc: "MSIs",
  684. before: []string{
  685. "MSICache/tailscale-1.0.0.msi",
  686. "MSICache/tailscale-1.1.0.msi",
  687. "MSICache/readme.txt",
  688. },
  689. glob: "MSICache/*.msi",
  690. after: []string{
  691. "MSICache/readme.txt",
  692. },
  693. },
  694. {
  695. desc: "SPKs",
  696. before: []string{
  697. "tmp/tailscale-update-1/tailscale-1.0.0.spk",
  698. "tmp/tailscale-update-2/tailscale-1.1.0.spk",
  699. "tmp/readme.txt",
  700. "tmp/tailscale-update-3",
  701. "tmp/tailscale-update-4/tailscale-1.3.0",
  702. },
  703. glob: "tmp/tailscale-update*/*.spk",
  704. after: []string{
  705. "tmp/readme.txt",
  706. "tmp/tailscale-update-3",
  707. "tmp/tailscale-update-4/tailscale-1.3.0",
  708. },
  709. },
  710. {
  711. desc: "empty-target",
  712. before: []string{},
  713. glob: "tmp/tailscale-update*/*.spk",
  714. after: []string{},
  715. },
  716. {
  717. desc: "keep-dirs",
  718. before: []string{
  719. "tmp/tailscale-update-1/tailscale-1.0.0.spk",
  720. },
  721. glob: "tmp/tailscale-update*",
  722. after: []string{
  723. "tmp/tailscale-update-1/tailscale-1.0.0.spk",
  724. },
  725. },
  726. {
  727. desc: "no-follow-symlinks",
  728. before: []string{
  729. "MSICache/tailscale-1.0.0.msi",
  730. "MSICache/tailscale-1.1.0.msi",
  731. "MSICache/readme.txt",
  732. },
  733. symlinks: map[string]string{
  734. "MSICache/tailscale-1.3.0.msi": "MSICache/tailscale-1.0.0.msi",
  735. "MSICache/tailscale-1.4.0.msi": "MSICache/readme.txt",
  736. },
  737. glob: "MSICache/*.msi",
  738. after: []string{
  739. "MSICache/tailscale-1.3.0.msi",
  740. "MSICache/tailscale-1.4.0.msi",
  741. "MSICache/readme.txt",
  742. },
  743. },
  744. }
  745. for _, tt := range tests {
  746. t.Run(tt.desc, func(t *testing.T) {
  747. dir := t.TempDir()
  748. for _, p := range tt.before {
  749. if err := os.MkdirAll(filepath.Join(dir, filepath.Dir(p)), 0700); err != nil {
  750. t.Fatal(err)
  751. }
  752. if err := os.WriteFile(filepath.Join(dir, p), []byte(tt.desc), 0600); err != nil {
  753. t.Fatal(err)
  754. }
  755. }
  756. for from, to := range tt.symlinks {
  757. if err := os.Symlink(filepath.Join(dir, to), filepath.Join(dir, from)); err != nil {
  758. t.Fatal(err)
  759. }
  760. }
  761. up := &Updater{Arguments: Arguments{Logf: t.Logf}}
  762. up.cleanupOldDownloads(filepath.Join(dir, tt.glob))
  763. var after []string
  764. if err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
  765. if !d.IsDir() {
  766. after = append(after, strings.TrimPrefix(filepath.ToSlash(path), filepath.ToSlash(dir)+"/"))
  767. }
  768. return nil
  769. }); err != nil {
  770. t.Fatal(err)
  771. }
  772. sort.Strings(after)
  773. sort.Strings(tt.after)
  774. if !slices.Equal(after, tt.after) {
  775. t.Errorf("got files after cleanup: %q, want: %q", after, tt.after)
  776. }
  777. })
  778. }
  779. }
  780. func TestParseUnraidPluginVersion(t *testing.T) {
  781. tests := []struct {
  782. plgPath string
  783. wantVer string
  784. wantErr string
  785. }{
  786. {plgPath: "testdata/tailscale-1.52.0.plg", wantVer: "1.52.0"},
  787. {plgPath: "testdata/tailscale-1.54.0.plg", wantVer: "1.54.0"},
  788. {plgPath: "testdata/tailscale-nover.plg", wantErr: "version not found in plg file"},
  789. {plgPath: "testdata/tailscale-nover-path-mentioned.plg", wantErr: "version not found in plg file"},
  790. }
  791. for _, tt := range tests {
  792. t.Run(tt.plgPath, func(t *testing.T) {
  793. got, err := parseUnraidPluginVersion(tt.plgPath)
  794. if got != tt.wantVer {
  795. t.Errorf("got version: %q, want %q", got, tt.wantVer)
  796. }
  797. var gotErr string
  798. if err != nil {
  799. gotErr = err.Error()
  800. }
  801. if gotErr != tt.wantErr {
  802. t.Errorf("got error: %q, want %q", gotErr, tt.wantErr)
  803. }
  804. })
  805. }
  806. }
  807. func TestConfirm(t *testing.T) {
  808. curTrack := CurrentTrack
  809. defer func() { CurrentTrack = curTrack }()
  810. tests := []struct {
  811. desc string
  812. fromTrack string
  813. toTrack string
  814. fromVer string
  815. toVer string
  816. confirm func(string) bool
  817. want bool
  818. }{
  819. {
  820. desc: "on latest stable",
  821. fromTrack: StableTrack,
  822. toTrack: StableTrack,
  823. fromVer: "1.66.0",
  824. toVer: "1.66.0",
  825. want: false,
  826. },
  827. {
  828. desc: "stable upgrade",
  829. fromTrack: StableTrack,
  830. toTrack: StableTrack,
  831. fromVer: "1.66.0",
  832. toVer: "1.68.0",
  833. want: true,
  834. },
  835. {
  836. desc: "unstable upgrade",
  837. fromTrack: UnstableTrack,
  838. toTrack: UnstableTrack,
  839. fromVer: "1.67.1",
  840. toVer: "1.67.2",
  841. want: true,
  842. },
  843. {
  844. desc: "from stable to unstable",
  845. fromTrack: StableTrack,
  846. toTrack: UnstableTrack,
  847. fromVer: "1.66.0",
  848. toVer: "1.67.1",
  849. want: true,
  850. },
  851. {
  852. desc: "from unstable to stable",
  853. fromTrack: UnstableTrack,
  854. toTrack: StableTrack,
  855. fromVer: "1.67.1",
  856. toVer: "1.66.0",
  857. want: true,
  858. },
  859. {
  860. desc: "confirm callback rejects",
  861. fromTrack: StableTrack,
  862. toTrack: StableTrack,
  863. fromVer: "1.66.0",
  864. toVer: "1.66.1",
  865. confirm: func(string) bool {
  866. return false
  867. },
  868. want: false,
  869. },
  870. {
  871. desc: "confirm callback allows",
  872. fromTrack: StableTrack,
  873. toTrack: StableTrack,
  874. fromVer: "1.66.0",
  875. toVer: "1.66.1",
  876. confirm: func(string) bool {
  877. return true
  878. },
  879. want: true,
  880. },
  881. {
  882. desc: "downgrade",
  883. fromTrack: StableTrack,
  884. toTrack: StableTrack,
  885. fromVer: "1.66.1",
  886. toVer: "1.66.0",
  887. want: false,
  888. },
  889. }
  890. for _, tt := range tests {
  891. t.Run(tt.desc, func(t *testing.T) {
  892. CurrentTrack = tt.fromTrack
  893. up := Updater{
  894. currentVersion: tt.fromVer,
  895. Arguments: Arguments{
  896. Track: tt.toTrack,
  897. Confirm: tt.confirm,
  898. Logf: t.Logf,
  899. },
  900. }
  901. if got := up.confirm(tt.toVer); got != tt.want {
  902. t.Errorf("got %v, want %v", got, tt.want)
  903. }
  904. })
  905. }
  906. }