resolvconf_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. package resolvconf
  2. import (
  3. "bytes"
  4. "io/fs"
  5. "net/netip"
  6. "os"
  7. "path/filepath"
  8. "runtime"
  9. "strings"
  10. "testing"
  11. "github.com/docker/docker/internal/sliceutil"
  12. "github.com/google/go-cmp/cmp/cmpopts"
  13. "gotest.tools/v3/assert"
  14. is "gotest.tools/v3/assert/cmp"
  15. "gotest.tools/v3/golden"
  16. )
  17. func TestRCOption(t *testing.T) {
  18. testcases := []struct {
  19. name string
  20. options string
  21. search string
  22. expFound bool
  23. expValue string
  24. }{
  25. {
  26. name: "Empty options",
  27. options: "",
  28. search: "ndots",
  29. },
  30. {
  31. name: "Not found",
  32. options: "ndots:0 edns0",
  33. search: "trust-ad",
  34. },
  35. {
  36. name: "Found with value",
  37. options: "ndots:0 edns0",
  38. search: "ndots",
  39. expFound: true,
  40. expValue: "0",
  41. },
  42. {
  43. name: "Found without value",
  44. options: "ndots:0 edns0",
  45. search: "edns0",
  46. expFound: true,
  47. expValue: "",
  48. },
  49. {
  50. name: "Found last value",
  51. options: "ndots:0 edns0 ndots:1",
  52. search: "ndots",
  53. expFound: true,
  54. expValue: "1",
  55. },
  56. }
  57. for _, tc := range testcases {
  58. t.Run(tc.name, func(t *testing.T) {
  59. rc, err := Parse(bytes.NewBuffer([]byte("options "+tc.options)), "")
  60. assert.NilError(t, err)
  61. value, found := rc.Option(tc.search)
  62. assert.Check(t, is.Equal(found, tc.expFound))
  63. assert.Check(t, is.Equal(value, tc.expValue))
  64. })
  65. }
  66. }
  67. func TestRCWrite(t *testing.T) {
  68. testcases := []struct {
  69. name string
  70. fileName string
  71. perm os.FileMode
  72. hashFileName string
  73. modify bool
  74. expUserModified bool
  75. }{
  76. {
  77. name: "Write with hash",
  78. fileName: "testfile",
  79. hashFileName: "testfile.hash",
  80. },
  81. {
  82. name: "Write with hash and modify",
  83. fileName: "testfile",
  84. hashFileName: "testfile.hash",
  85. modify: true,
  86. expUserModified: true,
  87. },
  88. {
  89. name: "Write without hash and modify",
  90. fileName: "testfile",
  91. modify: true,
  92. expUserModified: false,
  93. },
  94. {
  95. name: "Write perm",
  96. fileName: "testfile",
  97. perm: 0640,
  98. },
  99. }
  100. rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4")), "")
  101. assert.NilError(t, err)
  102. for _, tc := range testcases {
  103. t.Run(tc.name, func(t *testing.T) {
  104. tc := tc
  105. d := t.TempDir()
  106. path := filepath.Join(d, tc.fileName)
  107. var hashPath string
  108. if tc.hashFileName != "" {
  109. hashPath = filepath.Join(d, tc.hashFileName)
  110. }
  111. if tc.perm == 0 {
  112. tc.perm = 0644
  113. }
  114. err := rc.WriteFile(path, hashPath, tc.perm)
  115. assert.NilError(t, err)
  116. fi, err := os.Stat(path)
  117. assert.NilError(t, err)
  118. // Windows files won't have the expected perms.
  119. if runtime.GOOS != "windows" {
  120. assert.Check(t, is.Equal(fi.Mode(), tc.perm))
  121. }
  122. if tc.modify {
  123. err := os.WriteFile(path, []byte("modified"), 0644)
  124. assert.NilError(t, err)
  125. }
  126. um, err := UserModified(path, hashPath)
  127. assert.NilError(t, err)
  128. assert.Check(t, is.Equal(um, tc.expUserModified))
  129. })
  130. }
  131. }
  132. var a2s = sliceutil.Mapper(netip.Addr.String)
  133. var s2a = sliceutil.Mapper(netip.MustParseAddr)
  134. // Test that a resolv.conf file can be modified using OverrideXXX() methods
  135. // to modify nameservers/search/options directives, and tha options can be
  136. // added via AddOption().
  137. func TestRCModify(t *testing.T) {
  138. testcases := []struct {
  139. name string
  140. inputNS []string
  141. inputSearch []string
  142. inputOptions []string
  143. noOverrides bool // Whether to apply overrides (empty lists are valid overrides).
  144. overrideNS []string
  145. overrideSearch []string
  146. overrideOptions []string
  147. addOption string
  148. }{
  149. {
  150. name: "No content no overrides",
  151. inputNS: []string{},
  152. },
  153. {
  154. name: "No overrides",
  155. noOverrides: true,
  156. inputNS: []string{"1.2.3.4"},
  157. inputSearch: []string{"invalid"},
  158. inputOptions: []string{"ndots:0"},
  159. },
  160. {
  161. name: "Empty overrides",
  162. inputNS: []string{"1.2.3.4"},
  163. inputSearch: []string{"invalid"},
  164. inputOptions: []string{"ndots:0"},
  165. },
  166. {
  167. name: "Overrides",
  168. inputNS: []string{"1.2.3.4"},
  169. inputSearch: []string{"invalid"},
  170. inputOptions: []string{"ndots:0"},
  171. overrideNS: []string{"2.3.4.5", "fdba:acdd:587c::53"},
  172. overrideSearch: []string{"com", "invalid", "example"},
  173. overrideOptions: []string{"ndots:1", "edns0", "trust-ad"},
  174. },
  175. {
  176. name: "Add option no overrides",
  177. noOverrides: true,
  178. inputNS: []string{"1.2.3.4"},
  179. inputSearch: []string{"invalid"},
  180. inputOptions: []string{"ndots:0"},
  181. addOption: "attempts:3",
  182. },
  183. }
  184. for _, tc := range testcases {
  185. t.Run(tc.name, func(t *testing.T) {
  186. tc := tc
  187. var input string
  188. if len(tc.inputNS) != 0 {
  189. for _, ns := range tc.inputNS {
  190. input += "nameserver " + ns + "\n"
  191. }
  192. }
  193. if len(tc.inputSearch) != 0 {
  194. input += "search " + strings.Join(tc.inputSearch, " ") + "\n"
  195. }
  196. if len(tc.inputOptions) != 0 {
  197. input += "options " + strings.Join(tc.inputOptions, " ") + "\n"
  198. }
  199. rc, err := Parse(bytes.NewBuffer([]byte(input)), "")
  200. assert.NilError(t, err)
  201. assert.Check(t, is.DeepEqual(a2s(rc.NameServers()), tc.inputNS))
  202. assert.Check(t, is.DeepEqual(rc.Search(), tc.inputSearch))
  203. assert.Check(t, is.DeepEqual(rc.Options(), tc.inputOptions))
  204. if !tc.noOverrides {
  205. overrideNS := s2a(tc.overrideNS)
  206. rc.OverrideNameServers(overrideNS)
  207. rc.OverrideSearch(tc.overrideSearch)
  208. rc.OverrideOptions(tc.overrideOptions)
  209. assert.Check(t, is.DeepEqual(rc.NameServers(), overrideNS, cmpopts.EquateEmpty(), cmpopts.EquateComparable(netip.Addr{})))
  210. assert.Check(t, is.DeepEqual(rc.Search(), tc.overrideSearch, cmpopts.EquateEmpty()))
  211. assert.Check(t, is.DeepEqual(rc.Options(), tc.overrideOptions, cmpopts.EquateEmpty()))
  212. }
  213. if tc.addOption != "" {
  214. options := rc.Options()
  215. rc.AddOption(tc.addOption)
  216. assert.Check(t, is.DeepEqual(rc.Options(), append(options, tc.addOption), cmpopts.EquateEmpty()))
  217. }
  218. d := t.TempDir()
  219. path := filepath.Join(d, "resolv.conf")
  220. err = rc.WriteFile(path, "", 0644)
  221. assert.NilError(t, err)
  222. content, err := os.ReadFile(path)
  223. assert.NilError(t, err)
  224. assert.Check(t, golden.String(string(content), t.Name()+".golden"))
  225. })
  226. }
  227. }
  228. func TestRCTransformForLegacyNw(t *testing.T) {
  229. testcases := []struct {
  230. name string
  231. input string
  232. ipv6 bool
  233. overrideNS []string
  234. }{
  235. {
  236. name: "Routable IPv4 only",
  237. input: "nameserver 10.0.0.1",
  238. },
  239. {
  240. name: "Routable IPv4 and IPv6, ipv6 enabled",
  241. input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
  242. ipv6: true,
  243. },
  244. {
  245. name: "Routable IPv4 and IPv6, ipv6 disabled",
  246. input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
  247. ipv6: false,
  248. },
  249. {
  250. name: "IPv4 localhost, ipv6 disabled",
  251. input: "nameserver 127.0.0.53",
  252. ipv6: false,
  253. },
  254. {
  255. name: "IPv4 localhost, ipv6 enabled",
  256. input: "nameserver 127.0.0.53",
  257. ipv6: true,
  258. },
  259. {
  260. name: "IPv4 and IPv6 localhost, ipv6 disabled",
  261. input: "nameserver 127.0.0.53\nnameserver ::1",
  262. ipv6: false,
  263. },
  264. {
  265. name: "IPv4 and IPv6 localhost, ipv6 enabled",
  266. input: "nameserver 127.0.0.53\nnameserver ::1",
  267. ipv6: true,
  268. },
  269. {
  270. name: "IPv4 localhost, IPv6 routeable, ipv6 enabled",
  271. input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
  272. ipv6: true,
  273. },
  274. {
  275. name: "IPv4 localhost, IPv6 routeable, ipv6 disabled",
  276. input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
  277. ipv6: false,
  278. },
  279. {
  280. name: "Override nameservers",
  281. input: "nameserver 127.0.0.53",
  282. overrideNS: []string{"127.0.0.1", "::1"},
  283. ipv6: false,
  284. },
  285. }
  286. for _, tc := range testcases {
  287. t.Run(tc.name, func(t *testing.T) {
  288. tc := tc
  289. rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf")
  290. assert.NilError(t, err)
  291. if tc.overrideNS != nil {
  292. rc.OverrideNameServers(s2a(tc.overrideNS))
  293. }
  294. rc.TransformForLegacyNw(tc.ipv6)
  295. d := t.TempDir()
  296. path := filepath.Join(d, "resolv.conf")
  297. err = rc.WriteFile(path, "", 0644)
  298. assert.NilError(t, err)
  299. content, err := os.ReadFile(path)
  300. assert.NilError(t, err)
  301. assert.Check(t, golden.String(string(content), t.Name()+".golden"))
  302. })
  303. }
  304. }
  305. func TestRCTransformForIntNS(t *testing.T) {
  306. mke := func(addr string, hostLoopback bool) ExtDNSEntry {
  307. return ExtDNSEntry{
  308. Addr: netip.MustParseAddr(addr),
  309. HostLoopback: hostLoopback,
  310. }
  311. }
  312. testcases := []struct {
  313. name string
  314. input string
  315. intNameServer string
  316. ipv6 bool
  317. overrideNS []string
  318. overrideOptions []string
  319. reqdOptions []string
  320. expExtServers []ExtDNSEntry
  321. expErr string
  322. }{
  323. {
  324. name: "IPv4 only",
  325. input: "nameserver 10.0.0.1",
  326. expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
  327. },
  328. {
  329. name: "IPv4 and IPv6, ipv6 enabled",
  330. input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
  331. ipv6: true,
  332. expExtServers: []ExtDNSEntry{
  333. mke("10.0.0.1", false),
  334. mke("fdb6:b8fe:b528::1", false),
  335. },
  336. },
  337. {
  338. name: "IPv4 and IPv6, ipv6 disabled",
  339. input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
  340. ipv6: false,
  341. expExtServers: []ExtDNSEntry{
  342. mke("10.0.0.1", false),
  343. mke("fdb6:b8fe:b528::1", true),
  344. },
  345. },
  346. {
  347. name: "IPv4 localhost",
  348. input: "nameserver 127.0.0.53",
  349. ipv6: false,
  350. expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
  351. },
  352. {
  353. // Overriding the nameserver with a localhost address means use the container's
  354. // loopback interface, not the host's.
  355. name: "IPv4 localhost override",
  356. input: "nameserver 10.0.0.1",
  357. ipv6: false,
  358. overrideNS: []string{"127.0.0.53"},
  359. expExtServers: []ExtDNSEntry{mke("127.0.0.53", false)},
  360. },
  361. {
  362. name: "IPv4 localhost, ipv6 enabled",
  363. input: "nameserver 127.0.0.53",
  364. ipv6: true,
  365. expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
  366. },
  367. {
  368. name: "IPv6 addr, IPv6 enabled",
  369. input: "nameserver fd14:6e0e:f855::1",
  370. ipv6: true,
  371. expExtServers: []ExtDNSEntry{mke("fd14:6e0e:f855::1", false)},
  372. },
  373. {
  374. name: "IPv4 and IPv6 localhost, IPv6 disabled",
  375. input: "nameserver 127.0.0.53\nnameserver ::1",
  376. ipv6: false,
  377. expExtServers: []ExtDNSEntry{
  378. mke("127.0.0.53", true),
  379. mke("::1", true),
  380. },
  381. },
  382. {
  383. name: "IPv4 and IPv6 localhost, ipv6 enabled",
  384. input: "nameserver 127.0.0.53\nnameserver ::1",
  385. ipv6: true,
  386. expExtServers: []ExtDNSEntry{
  387. mke("127.0.0.53", true),
  388. mke("::1", true),
  389. },
  390. },
  391. {
  392. name: "IPv4 localhost, IPv6 private, IPv6 enabled",
  393. input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
  394. ipv6: true,
  395. expExtServers: []ExtDNSEntry{
  396. mke("127.0.0.53", true),
  397. mke("fd3e:2d1a:1f5a::1", false),
  398. },
  399. },
  400. {
  401. name: "IPv4 localhost, IPv6 private, IPv6 disabled",
  402. input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
  403. ipv6: false,
  404. expExtServers: []ExtDNSEntry{
  405. mke("127.0.0.53", true),
  406. mke("fd3e:2d1a:1f5a::1", true),
  407. },
  408. },
  409. {
  410. name: "No host nameserver, no iv6",
  411. input: "",
  412. ipv6: false,
  413. expExtServers: []ExtDNSEntry{
  414. mke("8.8.8.8", false),
  415. mke("8.8.4.4", false),
  416. },
  417. },
  418. {
  419. name: "No host nameserver, iv6",
  420. input: "",
  421. ipv6: true,
  422. expExtServers: []ExtDNSEntry{
  423. mke("8.8.8.8", false),
  424. mke("8.8.4.4", false),
  425. mke("2001:4860:4860::8888", false),
  426. mke("2001:4860:4860::8844", false),
  427. },
  428. },
  429. {
  430. name: "ndots present and required",
  431. input: "nameserver 127.0.0.53\noptions ndots:1",
  432. reqdOptions: []string{"ndots:0"},
  433. expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
  434. },
  435. {
  436. name: "ndots missing but required",
  437. input: "nameserver 127.0.0.53",
  438. reqdOptions: []string{"ndots:0"},
  439. expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
  440. },
  441. {
  442. name: "ndots host, override and required",
  443. input: "nameserver 127.0.0.53",
  444. reqdOptions: []string{"ndots:0"},
  445. overrideOptions: []string{"ndots:2"},
  446. expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
  447. },
  448. {
  449. name: "Extra required options",
  450. input: "nameserver 127.0.0.53\noptions trust-ad",
  451. reqdOptions: []string{"ndots:0", "attempts:3", "edns0", "trust-ad"},
  452. expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
  453. },
  454. }
  455. for _, tc := range testcases {
  456. t.Run(tc.name, func(t *testing.T) {
  457. tc := tc
  458. rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf")
  459. assert.NilError(t, err)
  460. if tc.intNameServer == "" {
  461. tc.intNameServer = "127.0.0.11"
  462. }
  463. if len(tc.overrideNS) > 0 {
  464. rc.OverrideNameServers(s2a(tc.overrideNS))
  465. }
  466. if len(tc.overrideOptions) > 0 {
  467. rc.OverrideOptions(tc.overrideOptions)
  468. }
  469. intNS := netip.MustParseAddr(tc.intNameServer)
  470. extNameServers, err := rc.TransformForIntNS(tc.ipv6, intNS, tc.reqdOptions)
  471. if tc.expErr != "" {
  472. assert.Check(t, is.ErrorContains(err, tc.expErr))
  473. return
  474. }
  475. assert.NilError(t, err)
  476. d := t.TempDir()
  477. path := filepath.Join(d, "resolv.conf")
  478. err = rc.WriteFile(path, "", 0644)
  479. assert.NilError(t, err)
  480. content, err := os.ReadFile(path)
  481. assert.NilError(t, err)
  482. assert.Check(t, golden.String(string(content), t.Name()+".golden"))
  483. assert.Check(t, is.DeepEqual(extNameServers, tc.expExtServers,
  484. cmpopts.EquateComparable(netip.Addr{})))
  485. })
  486. }
  487. }
  488. // Check that invalid ndots options in the host's file are ignored, unless
  489. // starting the internal resolver (which requires an ndots option), in which
  490. // case invalid ndots should be replaced.
  491. func TestRCTransformForIntNSInvalidNdots(t *testing.T) {
  492. testcases := []struct {
  493. name string
  494. options string
  495. reqdOptions []string
  496. expVal string
  497. expOptions []string
  498. expNDotsFrom string
  499. }{
  500. {
  501. name: "Negative value",
  502. options: "options ndots:-1",
  503. expOptions: []string{"ndots:-1"},
  504. expVal: "-1",
  505. expNDotsFrom: "host",
  506. },
  507. {
  508. name: "Invalid values with reqd ndots",
  509. options: "options ndots:-1 foo:bar ndots ndots:",
  510. reqdOptions: []string{"ndots:2"},
  511. expVal: "2",
  512. expNDotsFrom: "internal",
  513. expOptions: []string{"foo:bar", "ndots:2"},
  514. },
  515. {
  516. name: "Valid value with reqd ndots",
  517. options: "options ndots:1 foo:bar ndots ndots:",
  518. reqdOptions: []string{"ndots:2"},
  519. expVal: "1",
  520. expNDotsFrom: "host",
  521. expOptions: []string{"ndots:1", "foo:bar"},
  522. },
  523. }
  524. for _, tc := range testcases {
  525. t.Run(tc.name, func(t *testing.T) {
  526. content := "nameserver 8.8.8.8\n" + tc.options
  527. rc, err := Parse(bytes.NewBuffer([]byte(content)), "/etc/resolv.conf")
  528. assert.NilError(t, err)
  529. _, err = rc.TransformForIntNS(false, netip.MustParseAddr("127.0.0.11"), tc.reqdOptions)
  530. assert.NilError(t, err)
  531. val, found := rc.Option("ndots")
  532. assert.Check(t, is.Equal(found, true))
  533. assert.Check(t, is.Equal(val, tc.expVal))
  534. assert.Check(t, is.Equal(rc.md.NDotsFrom, tc.expNDotsFrom))
  535. assert.Check(t, is.DeepEqual(rc.options, tc.expOptions))
  536. })
  537. }
  538. }
  539. func TestRCRead(t *testing.T) {
  540. d := t.TempDir()
  541. path := filepath.Join(d, "resolv.conf")
  542. // Try to read a nonexistent file, equivalent to an empty file.
  543. _, err := Load(path)
  544. assert.Check(t, is.ErrorIs(err, fs.ErrNotExist))
  545. err = os.WriteFile(path, []byte("options edns0"), 0644)
  546. assert.NilError(t, err)
  547. // Read that file in the constructor.
  548. rc, err := Load(path)
  549. assert.NilError(t, err)
  550. assert.Check(t, is.DeepEqual(rc.Options(), []string{"edns0"}))
  551. // Pass in an os.File, check the path is extracted.
  552. file, err := os.Open(path)
  553. assert.NilError(t, err)
  554. defer file.Close()
  555. rc, err = Parse(file, "")
  556. assert.NilError(t, err)
  557. assert.Check(t, is.Equal(rc.md.SourcePath, path))
  558. }
  559. func TestRCInvalidNS(t *testing.T) {
  560. d := t.TempDir()
  561. // A resolv.conf with an invalid nameserver address.
  562. rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4.5")), "")
  563. assert.NilError(t, err)
  564. path := filepath.Join(d, "resolv.conf")
  565. err = rc.WriteFile(path, "", 0644)
  566. assert.NilError(t, err)
  567. content, err := os.ReadFile(path)
  568. assert.NilError(t, err)
  569. assert.Check(t, golden.String(string(content), t.Name()+".golden"))
  570. }
  571. func TestRCSetHeader(t *testing.T) {
  572. rc, err := Parse(bytes.NewBuffer([]byte("nameserver 127.0.0.53")), "/etc/resolv.conf")
  573. assert.NilError(t, err)
  574. rc.SetHeader("# This is a comment.")
  575. d := t.TempDir()
  576. path := filepath.Join(d, "resolv.conf")
  577. err = rc.WriteFile(path, "", 0644)
  578. assert.NilError(t, err)
  579. content, err := os.ReadFile(path)
  580. assert.NilError(t, err)
  581. assert.Check(t, golden.String(string(content), t.Name()+".golden"))
  582. }
  583. func TestRCUnknownDirectives(t *testing.T) {
  584. const input = `
  585. something unexpected
  586. nameserver 127.0.0.53
  587. options ndots:1
  588. unrecognised thing
  589. `
  590. rc, err := Parse(bytes.NewBuffer([]byte(input)), "/etc/resolv.conf")
  591. assert.NilError(t, err)
  592. d := t.TempDir()
  593. path := filepath.Join(d, "resolv.conf")
  594. err = rc.WriteFile(path, "", 0644)
  595. assert.NilError(t, err)
  596. content, err := os.ReadFile(path)
  597. assert.NilError(t, err)
  598. assert.Check(t, golden.String(string(content), t.Name()+".golden"))
  599. }