clientupdate_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  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. for _, tt := range tests {
  242. t.Run(tt.desc, func(t *testing.T) {
  243. got, err := parseAlpinePackageVersion([]byte(tt.out))
  244. if err == nil && tt.wantErr {
  245. t.Fatalf("got nil error and version %q, want non-nil error", got)
  246. }
  247. if err != nil && !tt.wantErr {
  248. t.Fatalf("got error: %q, want nil", err)
  249. }
  250. if got != tt.want {
  251. t.Fatalf("got version: %q, want %q", got, tt.want)
  252. }
  253. })
  254. }
  255. }
  256. func TestSynoArch(t *testing.T) {
  257. tests := []struct {
  258. goarch string
  259. synoinfoUnique string
  260. want string
  261. wantErr bool
  262. }{
  263. {goarch: "amd64", synoinfoUnique: "synology_x86_224", want: "x86_64"},
  264. {goarch: "arm64", synoinfoUnique: "synology_armv8_124", want: "armv8"},
  265. {goarch: "386", synoinfoUnique: "synology_i686_415play", want: "i686"},
  266. {goarch: "arm", synoinfoUnique: "synology_88f6281_213air", want: "88f6281"},
  267. {goarch: "arm", synoinfoUnique: "synology_88f6282_413j", want: "88f6282"},
  268. {goarch: "arm", synoinfoUnique: "synology_hi3535_NVR1218", want: "hi3535"},
  269. {goarch: "arm", synoinfoUnique: "synology_alpine_1517", want: "alpine"},
  270. {goarch: "arm", synoinfoUnique: "synology_armada370_216se", want: "armada370"},
  271. {goarch: "arm", synoinfoUnique: "synology_armada375_115", want: "armada375"},
  272. {goarch: "arm", synoinfoUnique: "synology_armada38x_419slim", want: "armada38x"},
  273. {goarch: "arm", synoinfoUnique: "synology_armadaxp_RS815", want: "armadaxp"},
  274. {goarch: "arm", synoinfoUnique: "synology_comcerto2k_414j", want: "comcerto2k"},
  275. {goarch: "arm", synoinfoUnique: "synology_monaco_216play", want: "monaco"},
  276. {goarch: "ppc64", synoinfoUnique: "synology_qoriq_413", wantErr: true},
  277. }
  278. for _, tt := range tests {
  279. t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.synoinfoUnique), func(t *testing.T) {
  280. synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
  281. if err := os.WriteFile(
  282. synoinfoConfPath,
  283. []byte(fmt.Sprintf("unique=%q\n", tt.synoinfoUnique)),
  284. 0600,
  285. ); err != nil {
  286. t.Fatal(err)
  287. }
  288. got, err := synoArch(tt.goarch, synoinfoConfPath)
  289. if err != nil {
  290. if !tt.wantErr {
  291. t.Fatalf("got unexpected error %v", err)
  292. }
  293. return
  294. }
  295. if tt.wantErr {
  296. t.Fatalf("got %q, expected an error", got)
  297. }
  298. if got != tt.want {
  299. t.Errorf("got %q, want %q", got, tt.want)
  300. }
  301. })
  302. }
  303. }
  304. func TestParseSynoinfo(t *testing.T) {
  305. tests := []struct {
  306. desc string
  307. content string
  308. want string
  309. wantErr bool
  310. }{
  311. {
  312. desc: "double-quoted",
  313. content: `
  314. company_title="Synology"
  315. unique="synology_88f6281_213air"
  316. `,
  317. want: "88f6281",
  318. },
  319. {
  320. desc: "single-quoted",
  321. content: `
  322. company_title="Synology"
  323. unique='synology_88f6281_213air'
  324. `,
  325. want: "88f6281",
  326. },
  327. {
  328. desc: "unquoted",
  329. content: `
  330. company_title="Synology"
  331. unique=synology_88f6281_213air
  332. `,
  333. want: "88f6281",
  334. },
  335. {
  336. desc: "missing unique",
  337. content: `
  338. company_title="Synology"
  339. `,
  340. wantErr: true,
  341. },
  342. {
  343. desc: "empty unique",
  344. content: `
  345. company_title="Synology"
  346. unique=
  347. `,
  348. wantErr: true,
  349. },
  350. {
  351. desc: "empty unique double-quoted",
  352. content: `
  353. company_title="Synology"
  354. unique=""
  355. `,
  356. wantErr: true,
  357. },
  358. {
  359. desc: "empty unique single-quoted",
  360. content: `
  361. company_title="Synology"
  362. unique=''
  363. `,
  364. wantErr: true,
  365. },
  366. {
  367. desc: "malformed unique",
  368. content: `
  369. company_title="Synology"
  370. unique="synology_88f6281"
  371. `,
  372. wantErr: true,
  373. },
  374. {
  375. desc: "empty file",
  376. content: ``,
  377. wantErr: true,
  378. },
  379. {
  380. desc: "empty lines and comments",
  381. content: `
  382. # In a file named synoinfo? Shocking!
  383. company_title="Synology"
  384. # unique= is_a_field_that_follows
  385. unique="synology_88f6281_213air"
  386. `,
  387. want: "88f6281",
  388. },
  389. }
  390. for _, tt := range tests {
  391. t.Run(tt.desc, func(t *testing.T) {
  392. synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
  393. if err := os.WriteFile(synoinfoConfPath, []byte(tt.content), 0600); err != nil {
  394. t.Fatal(err)
  395. }
  396. got, err := parseSynoinfo(synoinfoConfPath)
  397. if err != nil {
  398. if !tt.wantErr {
  399. t.Fatalf("got unexpected error %v", err)
  400. }
  401. return
  402. }
  403. if tt.wantErr {
  404. t.Fatalf("got %q, expected an error", got)
  405. }
  406. if got != tt.want {
  407. t.Errorf("got %q, want %q", got, tt.want)
  408. }
  409. })
  410. }
  411. }
  412. func TestUnpackLinuxTarball(t *testing.T) {
  413. oldBinaryPaths := binaryPaths
  414. t.Cleanup(func() { binaryPaths = oldBinaryPaths })
  415. tests := []struct {
  416. desc string
  417. tarball map[string]string
  418. before map[string]string
  419. after map[string]string
  420. wantErr bool
  421. }{
  422. {
  423. desc: "success",
  424. before: map[string]string{
  425. "tailscale": "v1",
  426. "tailscaled": "v1",
  427. },
  428. tarball: map[string]string{
  429. "/usr/bin/tailscale": "v2",
  430. "/usr/bin/tailscaled": "v2",
  431. },
  432. after: map[string]string{
  433. "tailscale": "v2",
  434. "tailscaled": "v2",
  435. },
  436. },
  437. {
  438. desc: "don't touch unrelated files",
  439. before: map[string]string{
  440. "tailscale": "v1",
  441. "tailscaled": "v1",
  442. "foo": "bar",
  443. },
  444. tarball: map[string]string{
  445. "/usr/bin/tailscale": "v2",
  446. "/usr/bin/tailscaled": "v2",
  447. },
  448. after: map[string]string{
  449. "tailscale": "v2",
  450. "tailscaled": "v2",
  451. "foo": "bar",
  452. },
  453. },
  454. {
  455. desc: "unmodified",
  456. before: map[string]string{
  457. "tailscale": "v1",
  458. "tailscaled": "v1",
  459. },
  460. tarball: map[string]string{
  461. "/usr/bin/tailscale": "v1",
  462. "/usr/bin/tailscaled": "v1",
  463. },
  464. after: map[string]string{
  465. "tailscale": "v1",
  466. "tailscaled": "v1",
  467. },
  468. },
  469. {
  470. desc: "ignore extra tarball files",
  471. before: map[string]string{
  472. "tailscale": "v1",
  473. "tailscaled": "v1",
  474. },
  475. tarball: map[string]string{
  476. "/usr/bin/tailscale": "v2",
  477. "/usr/bin/tailscaled": "v2",
  478. "/systemd/tailscaled.service": "v2",
  479. },
  480. after: map[string]string{
  481. "tailscale": "v2",
  482. "tailscaled": "v2",
  483. },
  484. },
  485. {
  486. desc: "tarball missing tailscaled",
  487. before: map[string]string{
  488. "tailscale": "v1",
  489. "tailscaled": "v1",
  490. },
  491. tarball: map[string]string{
  492. "/usr/bin/tailscale": "v2",
  493. },
  494. after: map[string]string{
  495. "tailscale": "v1",
  496. "tailscale.new": "v2",
  497. "tailscaled": "v1",
  498. },
  499. wantErr: true,
  500. },
  501. {
  502. desc: "duplicate tailscale binary",
  503. before: map[string]string{
  504. "tailscale": "v1",
  505. "tailscaled": "v1",
  506. },
  507. tarball: map[string]string{
  508. "/usr/bin/tailscale": "v2",
  509. "/usr/sbin/tailscale": "v2",
  510. "/usr/bin/tailscaled": "v2",
  511. },
  512. after: map[string]string{
  513. "tailscale": "v1",
  514. "tailscale.new": "v2",
  515. "tailscaled": "v1",
  516. "tailscaled.new": "v2",
  517. },
  518. wantErr: true,
  519. },
  520. {
  521. desc: "empty archive",
  522. before: map[string]string{
  523. "tailscale": "v1",
  524. "tailscaled": "v1",
  525. },
  526. tarball: map[string]string{},
  527. after: map[string]string{
  528. "tailscale": "v1",
  529. "tailscaled": "v1",
  530. },
  531. wantErr: true,
  532. },
  533. }
  534. for _, tt := range tests {
  535. t.Run(tt.desc, func(t *testing.T) {
  536. // Swap out binaryPaths function to point at dummy file paths.
  537. tmp := t.TempDir()
  538. tailscalePath := filepath.Join(tmp, "tailscale")
  539. tailscaledPath := filepath.Join(tmp, "tailscaled")
  540. binaryPaths = func() (string, string, error) {
  541. return tailscalePath, tailscaledPath, nil
  542. }
  543. for name, content := range tt.before {
  544. if err := os.WriteFile(filepath.Join(tmp, name), []byte(content), 0755); err != nil {
  545. t.Fatal(err)
  546. }
  547. }
  548. tarPath := filepath.Join(tmp, "tailscale.tgz")
  549. genTarball(t, tarPath, tt.tarball)
  550. up := &Updater{Arguments: Arguments{Logf: t.Logf}}
  551. err := up.unpackLinuxTarball(tarPath)
  552. if err != nil {
  553. if !tt.wantErr {
  554. t.Fatalf("unexpected error: %v", err)
  555. }
  556. } else if tt.wantErr {
  557. t.Fatalf("unpack succeeded, expected an error")
  558. }
  559. gotAfter := make(map[string]string)
  560. err = filepath.WalkDir(tmp, func(path string, d fs.DirEntry, err error) error {
  561. if err != nil {
  562. return err
  563. }
  564. if d.Type().IsDir() {
  565. return nil
  566. }
  567. if path == tarPath {
  568. return nil
  569. }
  570. content, err := os.ReadFile(path)
  571. if err != nil {
  572. return err
  573. }
  574. path = filepath.ToSlash(path)
  575. base := filepath.ToSlash(tmp)
  576. gotAfter[strings.TrimPrefix(path, base+"/")] = string(content)
  577. return nil
  578. })
  579. if err != nil {
  580. t.Fatal(err)
  581. }
  582. if !maps.Equal(gotAfter, tt.after) {
  583. t.Errorf("files after unpack: %+v, want %+v", gotAfter, tt.after)
  584. }
  585. })
  586. }
  587. }
  588. func genTarball(t *testing.T, path string, files map[string]string) {
  589. f, err := os.Create(path)
  590. if err != nil {
  591. t.Fatal(err)
  592. }
  593. defer f.Close()
  594. gw := gzip.NewWriter(f)
  595. defer gw.Close()
  596. tw := tar.NewWriter(gw)
  597. defer tw.Close()
  598. for file, content := range files {
  599. if err := tw.WriteHeader(&tar.Header{
  600. Name: file,
  601. Size: int64(len(content)),
  602. Mode: 0755,
  603. }); err != nil {
  604. t.Fatal(err)
  605. }
  606. if _, err := tw.Write([]byte(content)); err != nil {
  607. t.Fatal(err)
  608. }
  609. }
  610. }
  611. func TestWriteFileOverwrite(t *testing.T) {
  612. path := filepath.Join(t.TempDir(), "test")
  613. for i := 0; i < 2; i++ {
  614. content := fmt.Sprintf("content %d", i)
  615. if err := writeFile(strings.NewReader(content), path, 0600); err != nil {
  616. t.Fatal(err)
  617. }
  618. got, err := os.ReadFile(path)
  619. if err != nil {
  620. t.Fatal(err)
  621. }
  622. if string(got) != content {
  623. t.Errorf("got content: %q, want: %q", got, content)
  624. }
  625. }
  626. }
  627. func TestWriteFileSymlink(t *testing.T) {
  628. // Test for a malicious symlink at the destination path.
  629. // f2 points to f1 and writeFile(f2) should not end up overwriting f1.
  630. tmp := t.TempDir()
  631. f1 := filepath.Join(tmp, "f1")
  632. if err := os.WriteFile(f1, []byte("old"), 0600); err != nil {
  633. t.Fatal(err)
  634. }
  635. f2 := filepath.Join(tmp, "f2")
  636. if err := os.Symlink(f1, f2); err != nil {
  637. t.Fatal(err)
  638. }
  639. if err := writeFile(strings.NewReader("new"), f2, 0600); err != nil {
  640. t.Errorf("writeFile(%q) failed: %v", f2, err)
  641. }
  642. want := map[string]string{
  643. f1: "old",
  644. f2: "new",
  645. }
  646. for f, content := range want {
  647. got, err := os.ReadFile(f)
  648. if err != nil {
  649. t.Fatal(err)
  650. }
  651. if string(got) != content {
  652. t.Errorf("%q: got content %q, want %q", f, got, content)
  653. }
  654. }
  655. }
  656. func TestCleanupOldDownloads(t *testing.T) {
  657. tests := []struct {
  658. desc string
  659. before []string
  660. symlinks map[string]string
  661. glob string
  662. after []string
  663. }{
  664. {
  665. desc: "MSIs",
  666. before: []string{
  667. "MSICache/tailscale-1.0.0.msi",
  668. "MSICache/tailscale-1.1.0.msi",
  669. "MSICache/readme.txt",
  670. },
  671. glob: "MSICache/*.msi",
  672. after: []string{
  673. "MSICache/readme.txt",
  674. },
  675. },
  676. {
  677. desc: "SPKs",
  678. before: []string{
  679. "tmp/tailscale-update-1/tailscale-1.0.0.spk",
  680. "tmp/tailscale-update-2/tailscale-1.1.0.spk",
  681. "tmp/readme.txt",
  682. "tmp/tailscale-update-3",
  683. "tmp/tailscale-update-4/tailscale-1.3.0",
  684. },
  685. glob: "tmp/tailscale-update*/*.spk",
  686. after: []string{
  687. "tmp/readme.txt",
  688. "tmp/tailscale-update-3",
  689. "tmp/tailscale-update-4/tailscale-1.3.0",
  690. },
  691. },
  692. {
  693. desc: "empty-target",
  694. before: []string{},
  695. glob: "tmp/tailscale-update*/*.spk",
  696. after: []string{},
  697. },
  698. {
  699. desc: "keep-dirs",
  700. before: []string{
  701. "tmp/tailscale-update-1/tailscale-1.0.0.spk",
  702. },
  703. glob: "tmp/tailscale-update*",
  704. after: []string{
  705. "tmp/tailscale-update-1/tailscale-1.0.0.spk",
  706. },
  707. },
  708. {
  709. desc: "no-follow-symlinks",
  710. before: []string{
  711. "MSICache/tailscale-1.0.0.msi",
  712. "MSICache/tailscale-1.1.0.msi",
  713. "MSICache/readme.txt",
  714. },
  715. symlinks: map[string]string{
  716. "MSICache/tailscale-1.3.0.msi": "MSICache/tailscale-1.0.0.msi",
  717. "MSICache/tailscale-1.4.0.msi": "MSICache/readme.txt",
  718. },
  719. glob: "MSICache/*.msi",
  720. after: []string{
  721. "MSICache/tailscale-1.3.0.msi",
  722. "MSICache/tailscale-1.4.0.msi",
  723. "MSICache/readme.txt",
  724. },
  725. },
  726. }
  727. for _, tt := range tests {
  728. t.Run(tt.desc, func(t *testing.T) {
  729. dir := t.TempDir()
  730. for _, p := range tt.before {
  731. if err := os.MkdirAll(filepath.Join(dir, filepath.Dir(p)), 0700); err != nil {
  732. t.Fatal(err)
  733. }
  734. if err := os.WriteFile(filepath.Join(dir, p), []byte(tt.desc), 0600); err != nil {
  735. t.Fatal(err)
  736. }
  737. }
  738. for from, to := range tt.symlinks {
  739. if err := os.Symlink(filepath.Join(dir, to), filepath.Join(dir, from)); err != nil {
  740. t.Fatal(err)
  741. }
  742. }
  743. up := &Updater{Arguments: Arguments{Logf: t.Logf}}
  744. up.cleanupOldDownloads(filepath.Join(dir, tt.glob))
  745. var after []string
  746. if err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
  747. if !d.IsDir() {
  748. after = append(after, strings.TrimPrefix(filepath.ToSlash(path), filepath.ToSlash(dir)+"/"))
  749. }
  750. return nil
  751. }); err != nil {
  752. t.Fatal(err)
  753. }
  754. sort.Strings(after)
  755. sort.Strings(tt.after)
  756. if !slices.Equal(after, tt.after) {
  757. t.Errorf("got files after cleanup: %q, want: %q", after, tt.after)
  758. }
  759. })
  760. }
  761. }
  762. func TestParseUnraidPluginVersion(t *testing.T) {
  763. tests := []struct {
  764. plgPath string
  765. wantVer string
  766. wantErr string
  767. }{
  768. {plgPath: "testdata/tailscale-1.52.0.plg", wantVer: "1.52.0"},
  769. {plgPath: "testdata/tailscale-1.54.0.plg", wantVer: "1.54.0"},
  770. {plgPath: "testdata/tailscale-nover.plg", wantErr: "version not found in plg file"},
  771. {plgPath: "testdata/tailscale-nover-path-mentioned.plg", wantErr: "version not found in plg file"},
  772. }
  773. for _, tt := range tests {
  774. t.Run(tt.plgPath, func(t *testing.T) {
  775. got, err := parseUnraidPluginVersion(tt.plgPath)
  776. if got != tt.wantVer {
  777. t.Errorf("got version: %q, want %q", got, tt.wantVer)
  778. }
  779. var gotErr string
  780. if err != nil {
  781. gotErr = err.Error()
  782. }
  783. if gotErr != tt.wantErr {
  784. t.Errorf("got error: %q, want %q", gotErr, tt.wantErr)
  785. }
  786. })
  787. }
  788. }