client_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package keepersecurity
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "reflect"
  19. "testing"
  20. ksm "github.com/keeper-security/secrets-manager-go/core"
  21. corev1 "k8s.io/api/core/v1"
  22. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  23. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  24. "github.com/external-secrets/external-secrets/pkg/provider/keepersecurity/fake"
  25. testingfake "github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
  26. )
  27. const (
  28. folderID = "a8ekf031k"
  29. validExistingRecord = "record0/login"
  30. invalidRecord = "record5/login"
  31. outputRecord0 = "{\"title\":\"record0\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":[{\"type\":\"host\",\"label\":\"host0\",\"value\":[{\"hostName\":\"mysql\",\"port\":\"3306\"}]}],\"files\":null}"
  32. outputRecord1 = "{\"title\":\"record1\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":[{\"type\":\"host\",\"label\":\"host1\",\"value\":[{\"hostName\":\"mysql\",\"port\":\"3306\"}]}],\"files\":null}"
  33. outputRecord2 = "{\"title\":\"record2\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":[{\"type\":\"host\",\"label\":\"host2\",\"value\":[{\"hostName\":\"mysql\",\"port\":\"3306\"}]}],\"files\":null}"
  34. record0 = "record0"
  35. record1 = "record1"
  36. record2 = "record2"
  37. LoginKey = "login"
  38. PasswordKey = "password"
  39. HostKeyFormat = "host%d"
  40. RecordNameFormat = "record%d"
  41. )
  42. func TestClientDeleteSecret(t *testing.T) {
  43. type fields struct {
  44. ksmClient SecurityClient
  45. folderID string
  46. }
  47. type args struct {
  48. ctx context.Context
  49. remoteRef esv1.PushSecretRemoteRef
  50. }
  51. tests := []struct {
  52. name string
  53. fields fields
  54. args args
  55. wantErr bool
  56. }{
  57. {
  58. name: "Delete valid secret",
  59. fields: fields{
  60. ksmClient: &fake.MockKeeperClient{
  61. DeleteSecretsFn: func(recrecordUids []string) (map[string]string, error) {
  62. return map[string]string{
  63. record0: record0,
  64. }, nil
  65. },
  66. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  67. return generateRecords()[:1], nil
  68. },
  69. },
  70. folderID: folderID,
  71. },
  72. args: args{
  73. context.Background(),
  74. &v1alpha1.PushSecretRemoteRef{
  75. RemoteKey: validExistingRecord,
  76. },
  77. },
  78. wantErr: false,
  79. },
  80. {
  81. name: "Delete secret with multiple matches by Name",
  82. fields: fields{
  83. ksmClient: &fake.MockKeeperClient{
  84. DeleteSecretsFn: func(recrecordUids []string) (map[string]string, error) {
  85. return map[string]string{
  86. record0: record0,
  87. }, nil
  88. },
  89. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  90. return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
  91. },
  92. },
  93. folderID: folderID,
  94. },
  95. args: args{
  96. context.Background(),
  97. &v1alpha1.PushSecretRemoteRef{
  98. RemoteKey: validExistingRecord,
  99. },
  100. },
  101. wantErr: true,
  102. },
  103. {
  104. name: "Delete non existing secret",
  105. fields: fields{
  106. ksmClient: &fake.MockKeeperClient{
  107. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  108. return nil, errors.New("failed")
  109. },
  110. },
  111. folderID: folderID,
  112. },
  113. args: args{
  114. context.Background(),
  115. &v1alpha1.PushSecretRemoteRef{
  116. RemoteKey: invalidRecord,
  117. },
  118. },
  119. wantErr: true,
  120. },
  121. }
  122. for _, tt := range tests {
  123. t.Run(tt.name, func(t *testing.T) {
  124. c := &Client{
  125. ksmClient: tt.fields.ksmClient,
  126. folderID: tt.fields.folderID,
  127. }
  128. if err := c.DeleteSecret(tt.args.ctx, tt.args.remoteRef); (err != nil) != tt.wantErr {
  129. t.Errorf("DeleteSecret() error = %v, wantErr %v", err, tt.wantErr)
  130. }
  131. })
  132. }
  133. }
  134. func TestClientGetAllSecrets(t *testing.T) {
  135. type fields struct {
  136. ksmClient SecurityClient
  137. folderID string
  138. }
  139. type args struct {
  140. ctx context.Context
  141. ref esv1.ExternalSecretFind
  142. }
  143. var path = "path_to_fail"
  144. tests := []struct {
  145. name string
  146. fields fields
  147. args args
  148. want map[string][]byte
  149. wantErr bool
  150. }{
  151. {
  152. name: "Tags not Implemented",
  153. fields: fields{
  154. ksmClient: &fake.MockKeeperClient{},
  155. folderID: folderID,
  156. },
  157. args: args{
  158. ctx: context.Background(),
  159. ref: esv1.ExternalSecretFind{
  160. Tags: map[string]string{
  161. "xxx": "yyy",
  162. },
  163. },
  164. },
  165. wantErr: true,
  166. },
  167. {
  168. name: "Path not Implemented",
  169. fields: fields{
  170. ksmClient: &fake.MockKeeperClient{},
  171. folderID: folderID,
  172. },
  173. args: args{
  174. ctx: context.Background(),
  175. ref: esv1.ExternalSecretFind{
  176. Path: &path,
  177. },
  178. },
  179. wantErr: true,
  180. },
  181. {
  182. name: "Get secrets with matching regex",
  183. fields: fields{
  184. ksmClient: &fake.MockKeeperClient{
  185. GetSecretsFn: func(strings []string) ([]*ksm.Record, error) {
  186. return generateRecords(), nil
  187. },
  188. },
  189. folderID: folderID,
  190. },
  191. args: args{
  192. ctx: context.Background(),
  193. ref: esv1.ExternalSecretFind{
  194. Name: &esv1.FindName{
  195. RegExp: "record",
  196. },
  197. },
  198. },
  199. want: map[string][]byte{
  200. record0: []byte(outputRecord0),
  201. record1: []byte(outputRecord1),
  202. record2: []byte(outputRecord2),
  203. },
  204. wantErr: false,
  205. },
  206. {
  207. name: "Get 1 secret with matching regex",
  208. fields: fields{
  209. ksmClient: &fake.MockKeeperClient{
  210. GetSecretsFn: func(strings []string) ([]*ksm.Record, error) {
  211. return generateRecords(), nil
  212. },
  213. },
  214. folderID: folderID,
  215. },
  216. args: args{
  217. ctx: context.Background(),
  218. ref: esv1.ExternalSecretFind{
  219. Name: &esv1.FindName{
  220. RegExp: record0,
  221. },
  222. },
  223. },
  224. want: map[string][]byte{
  225. record0: []byte(outputRecord0),
  226. },
  227. wantErr: false,
  228. },
  229. }
  230. for _, tt := range tests {
  231. t.Run(tt.name, func(t *testing.T) {
  232. c := &Client{
  233. ksmClient: tt.fields.ksmClient,
  234. folderID: tt.fields.folderID,
  235. }
  236. got, err := c.GetAllSecrets(tt.args.ctx, tt.args.ref)
  237. if (err != nil) != tt.wantErr {
  238. t.Errorf("GetAllSecrets() error = %v, wantErr %v", err, tt.wantErr)
  239. return
  240. }
  241. if !reflect.DeepEqual(got, tt.want) {
  242. t.Errorf("GetAllSecrets() got = %v, want %v", got, tt.want)
  243. }
  244. })
  245. }
  246. }
  247. func TestClientGetSecret(t *testing.T) {
  248. type fields struct {
  249. ksmClient SecurityClient
  250. folderID string
  251. }
  252. type args struct {
  253. ctx context.Context
  254. ref esv1.ExternalSecretDataRemoteRef
  255. }
  256. tests := []struct {
  257. name string
  258. fields fields
  259. args args
  260. want []byte
  261. wantErr bool
  262. }{
  263. {
  264. name: "Get Secret with a property",
  265. fields: fields{
  266. ksmClient: &fake.MockKeeperClient{
  267. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  268. return []*ksm.Record{generateRecords()[0]}, nil
  269. },
  270. },
  271. folderID: folderID,
  272. },
  273. args: args{
  274. ctx: context.Background(),
  275. ref: esv1.ExternalSecretDataRemoteRef{
  276. Key: record0,
  277. Property: LoginKey,
  278. },
  279. },
  280. want: []byte("foo"),
  281. wantErr: false,
  282. },
  283. {
  284. name: "Get Secret without property",
  285. fields: fields{
  286. ksmClient: &fake.MockKeeperClient{
  287. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  288. return []*ksm.Record{generateRecords()[0]}, nil
  289. },
  290. },
  291. folderID: folderID,
  292. },
  293. args: args{
  294. ctx: context.Background(),
  295. ref: esv1.ExternalSecretDataRemoteRef{
  296. Key: record0,
  297. },
  298. },
  299. want: []byte(outputRecord0),
  300. wantErr: false,
  301. },
  302. {
  303. name: "Get secret with multiple matches by ID",
  304. fields: fields{
  305. ksmClient: &fake.MockKeeperClient{
  306. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  307. return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
  308. },
  309. },
  310. folderID: folderID,
  311. },
  312. args: args{
  313. ctx: context.Background(),
  314. ref: esv1.ExternalSecretDataRemoteRef{
  315. Key: record0,
  316. },
  317. },
  318. want: []byte(outputRecord0),
  319. wantErr: false,
  320. },
  321. {
  322. name: "Get non existing secret",
  323. fields: fields{
  324. ksmClient: &fake.MockKeeperClient{
  325. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  326. return nil, errors.New("not found")
  327. },
  328. },
  329. folderID: folderID,
  330. },
  331. args: args{
  332. ctx: context.Background(),
  333. ref: esv1.ExternalSecretDataRemoteRef{
  334. Key: "record5",
  335. },
  336. },
  337. wantErr: true,
  338. },
  339. {
  340. name: "Get valid secret with non existing property",
  341. fields: fields{
  342. ksmClient: &fake.MockKeeperClient{
  343. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  344. return []*ksm.Record{generateRecords()[0]}, nil
  345. },
  346. },
  347. folderID: folderID,
  348. },
  349. args: args{
  350. ctx: context.Background(),
  351. ref: esv1.ExternalSecretDataRemoteRef{
  352. Key: record0,
  353. Property: "invalid",
  354. },
  355. },
  356. wantErr: true,
  357. },
  358. }
  359. for _, tt := range tests {
  360. t.Run(tt.name, func(t *testing.T) {
  361. c := &Client{
  362. ksmClient: tt.fields.ksmClient,
  363. folderID: tt.fields.folderID,
  364. }
  365. got, err := c.GetSecret(tt.args.ctx, tt.args.ref)
  366. if (err != nil) != tt.wantErr {
  367. t.Errorf("GetSecret() error = %v, wantErr %v", err, tt.wantErr)
  368. return
  369. }
  370. if !reflect.DeepEqual(got, tt.want) {
  371. t.Errorf("GetSecret() got = %v, want %v", got, tt.want)
  372. }
  373. })
  374. }
  375. }
  376. func TestClientGetSecretMap(t *testing.T) {
  377. type fields struct {
  378. ksmClient SecurityClient
  379. folderID string
  380. }
  381. type args struct {
  382. ctx context.Context
  383. ref esv1.ExternalSecretDataRemoteRef
  384. }
  385. tests := []struct {
  386. name string
  387. fields fields
  388. args args
  389. want map[string][]byte
  390. wantErr bool
  391. }{
  392. {
  393. name: "Get Secret with valid property",
  394. fields: fields{
  395. ksmClient: &fake.MockKeeperClient{
  396. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  397. return []*ksm.Record{generateRecords()[0]}, nil
  398. },
  399. },
  400. folderID: folderID,
  401. },
  402. args: args{
  403. ctx: context.Background(),
  404. ref: esv1.ExternalSecretDataRemoteRef{
  405. Key: record0,
  406. Property: LoginKey,
  407. },
  408. },
  409. want: map[string][]byte{
  410. LoginKey: []byte("foo"),
  411. },
  412. wantErr: false,
  413. },
  414. {
  415. name: "Get Secret without property",
  416. fields: fields{
  417. ksmClient: &fake.MockKeeperClient{
  418. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  419. return []*ksm.Record{generateRecords()[0]}, nil
  420. },
  421. },
  422. folderID: folderID,
  423. },
  424. args: args{
  425. ctx: context.Background(),
  426. ref: esv1.ExternalSecretDataRemoteRef{
  427. Key: record0,
  428. },
  429. },
  430. want: map[string][]byte{
  431. LoginKey: []byte("foo"),
  432. PasswordKey: []byte("bar"),
  433. fmt.Sprintf(HostKeyFormat, 0): []byte("{\"hostName\":\"mysql\",\"port\":\"3306\"}"),
  434. },
  435. wantErr: false,
  436. },
  437. {
  438. name: "Get non existing secret",
  439. fields: fields{
  440. ksmClient: &fake.MockKeeperClient{
  441. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  442. return nil, errors.New("not found")
  443. },
  444. },
  445. folderID: folderID,
  446. },
  447. args: args{
  448. ctx: context.Background(),
  449. ref: esv1.ExternalSecretDataRemoteRef{
  450. Key: "record5",
  451. },
  452. },
  453. wantErr: true,
  454. },
  455. {
  456. name: "Get Secret with invalid property",
  457. fields: fields{
  458. ksmClient: &fake.MockKeeperClient{
  459. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  460. return []*ksm.Record{generateRecords()[0]}, nil
  461. },
  462. },
  463. folderID: folderID,
  464. },
  465. args: args{
  466. ctx: context.Background(),
  467. ref: esv1.ExternalSecretDataRemoteRef{
  468. Key: record0,
  469. Property: "invalid",
  470. },
  471. },
  472. wantErr: true,
  473. },
  474. }
  475. for _, tt := range tests {
  476. t.Run(tt.name, func(t *testing.T) {
  477. c := &Client{
  478. ksmClient: tt.fields.ksmClient,
  479. folderID: tt.fields.folderID,
  480. }
  481. got, err := c.GetSecretMap(tt.args.ctx, tt.args.ref)
  482. if (err != nil) != tt.wantErr {
  483. t.Errorf("GetSecretMap() error = %v, wantErr %v", err, tt.wantErr)
  484. return
  485. }
  486. if !reflect.DeepEqual(got, tt.want) {
  487. t.Errorf("GetSecretMap() got = %v, want %v", got, tt.want)
  488. }
  489. })
  490. }
  491. }
  492. func TestClientPushSecret(t *testing.T) {
  493. secretKey := "secret-key"
  494. type fields struct {
  495. ksmClient SecurityClient
  496. folderID string
  497. }
  498. type args struct {
  499. value []byte
  500. data testingfake.PushSecretData
  501. }
  502. tests := []struct {
  503. name string
  504. fields fields
  505. args args
  506. wantErr bool
  507. }{
  508. {
  509. name: "Invalid remote ref",
  510. fields: fields{
  511. ksmClient: &fake.MockKeeperClient{},
  512. folderID: folderID,
  513. },
  514. args: args{
  515. data: testingfake.PushSecretData{
  516. SecretKey: secretKey,
  517. RemoteKey: record0,
  518. },
  519. value: []byte("foo"),
  520. },
  521. wantErr: true,
  522. },
  523. {
  524. name: "Push new valid secret",
  525. fields: fields{
  526. ksmClient: &fake.MockKeeperClient{
  527. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  528. return generateRecords()[0:0], nil
  529. },
  530. CreateSecretWithRecordDataFn: func(recUID, folderUid string, recordData *ksm.RecordCreate) (string, error) {
  531. return "record5", nil
  532. },
  533. },
  534. folderID: folderID,
  535. },
  536. args: args{
  537. data: testingfake.PushSecretData{
  538. SecretKey: secretKey,
  539. RemoteKey: invalidRecord,
  540. },
  541. value: []byte("foo"),
  542. },
  543. wantErr: false,
  544. },
  545. {
  546. name: "Push existing valid secret",
  547. fields: fields{
  548. ksmClient: &fake.MockKeeperClient{
  549. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  550. return generateRecords()[0:1], nil
  551. },
  552. SaveFn: func(record *ksm.Record) error {
  553. return nil
  554. },
  555. },
  556. folderID: folderID,
  557. },
  558. args: args{
  559. data: testingfake.PushSecretData{
  560. SecretKey: secretKey,
  561. RemoteKey: validExistingRecord,
  562. },
  563. value: []byte("foo2"),
  564. },
  565. wantErr: false,
  566. },
  567. {
  568. name: "Unable to push new valid secret with multiple matches by Name",
  569. fields: fields{
  570. ksmClient: &fake.MockKeeperClient{
  571. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  572. return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
  573. },
  574. },
  575. folderID: folderID,
  576. },
  577. args: args{
  578. data: testingfake.PushSecretData{
  579. SecretKey: secretKey,
  580. RemoteKey: validExistingRecord,
  581. },
  582. value: []byte("foo"),
  583. },
  584. wantErr: true,
  585. },
  586. {
  587. name: "Unable to push new valid secret",
  588. fields: fields{
  589. ksmClient: &fake.MockKeeperClient{
  590. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  591. return nil, errors.New("NotFound")
  592. },
  593. CreateSecretWithRecordDataFn: func(recUID, folderUID string, recordData *ksm.RecordCreate) (string, error) {
  594. return "", errors.New("Unable to push")
  595. },
  596. },
  597. folderID: folderID,
  598. },
  599. args: args{
  600. data: testingfake.PushSecretData{
  601. SecretKey: secretKey,
  602. RemoteKey: invalidRecord,
  603. },
  604. value: []byte("foo"),
  605. },
  606. wantErr: true,
  607. },
  608. {
  609. name: "Unable to save existing valid secret",
  610. fields: fields{
  611. ksmClient: &fake.MockKeeperClient{
  612. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  613. return generateRecords()[0], nil
  614. },
  615. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  616. return generateRecords()[0:1], nil
  617. },
  618. SaveFn: func(record *ksm.Record) error {
  619. return errors.New("Unable to save")
  620. },
  621. },
  622. folderID: folderID,
  623. },
  624. args: args{
  625. data: testingfake.PushSecretData{
  626. SecretKey: secretKey,
  627. RemoteKey: validExistingRecord,
  628. },
  629. value: []byte("foo2"),
  630. },
  631. wantErr: true,
  632. },
  633. }
  634. for _, tt := range tests {
  635. t.Run(tt.name, func(t *testing.T) {
  636. c := &Client{
  637. ksmClient: tt.fields.ksmClient,
  638. folderID: tt.fields.folderID,
  639. }
  640. s := &corev1.Secret{Data: map[string][]byte{secretKey: tt.args.value}}
  641. if err := c.PushSecret(context.Background(), s, tt.args.data); (err != nil) != tt.wantErr {
  642. t.Errorf("PushSecret() error = %v, wantErr %v", err, tt.wantErr)
  643. }
  644. })
  645. }
  646. }
  647. func generateRecords() []*ksm.Record {
  648. var records []*ksm.Record
  649. for i := 0; i < 3; i++ {
  650. var record ksm.Record
  651. if i == 0 {
  652. record = ksm.Record{
  653. Uid: fmt.Sprintf(RecordNameFormat, i),
  654. RecordDict: map[string]any{
  655. "type": externalSecretType,
  656. "folderUID": folderID,
  657. },
  658. }
  659. } else {
  660. record = ksm.Record{
  661. Uid: fmt.Sprintf(RecordNameFormat, i),
  662. RecordDict: map[string]any{
  663. "type": LoginType,
  664. "folderUID": folderID,
  665. },
  666. }
  667. }
  668. sec := fmt.Sprintf("{\"title\":\"record%d\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":[{\"type\":\"host\",\"label\":\"host%d\",\"value\":[{\"hostName\":\"mysql\",\"port\":\"3306\"}]}]}", i, i)
  669. record.SetTitle(fmt.Sprintf(RecordNameFormat, i))
  670. record.SetStandardFieldValue(LoginKey, "foo")
  671. record.SetStandardFieldValue(PasswordKey, "bar")
  672. record.RawJson = sec
  673. records = append(records, &record)
  674. }
  675. return records
  676. }