agi.git_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. package agi
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. "github.com/robertkrimen/otto"
  9. "imuslab.com/arozos/mod/filesystem"
  10. "imuslab.com/arozos/mod/git"
  11. )
  12. func TestExportStringSlice(t *testing.T) {
  13. vm := otto.New()
  14. tests := []struct {
  15. name string
  16. expression string
  17. want []string
  18. }{
  19. {name: "array of strings", expression: `["a.txt", "b/c.go"]`, want: []string{"a.txt", "b/c.go"}},
  20. {name: "empty array", expression: `[]`, want: []string{}},
  21. {name: "single string", expression: `"only.txt"`, want: []string{"only.txt"}},
  22. {name: "empty string", expression: `""`, want: []string{}},
  23. {name: "undefined", expression: `undefined`, want: []string{}},
  24. {name: "null", expression: `null`, want: []string{}},
  25. {name: "mixed types keep only strings", expression: `["a.txt", 5, null, "b.txt"]`, want: []string{"a.txt", "b.txt"}},
  26. {name: "number", expression: `42`, want: []string{}},
  27. }
  28. for _, test := range tests {
  29. t.Run(test.name, func(t *testing.T) {
  30. value, err := vm.Run(test.expression)
  31. if err != nil {
  32. t.Fatalf("cannot evaluate %q: %v", test.expression, err)
  33. }
  34. got := exportStringSlice(value)
  35. if len(got) != len(test.want) {
  36. t.Fatalf("exportStringSlice(%s) = %v, want %v", test.expression, got, test.want)
  37. }
  38. for i := range got {
  39. if got[i] != test.want[i] {
  40. t.Errorf("exportStringSlice(%s)[%d] = %q, want %q", test.expression, i, got[i], test.want[i])
  41. }
  42. }
  43. })
  44. }
  45. }
  46. func TestExportOptions(t *testing.T) {
  47. vm := otto.New()
  48. tests := []struct {
  49. name string
  50. expression string
  51. wantEntries int
  52. }{
  53. {name: "populated object", expression: `({username: "toby", remember: true})`, wantEntries: 2},
  54. {name: "empty object", expression: `({})`, wantEntries: 0},
  55. {name: "undefined", expression: `undefined`, wantEntries: 0},
  56. {name: "string is not an object", expression: `"nope"`, wantEntries: 0},
  57. {name: "number is not an object", expression: `7`, wantEntries: 0},
  58. }
  59. for _, test := range tests {
  60. t.Run(test.name, func(t *testing.T) {
  61. value, err := vm.Run(test.expression)
  62. if err != nil {
  63. t.Fatalf("cannot evaluate %q: %v", test.expression, err)
  64. }
  65. options := exportOptions(value)
  66. if options == nil {
  67. t.Fatalf("exportOptions(%s) = nil, want a usable map", test.expression)
  68. }
  69. if len(options) != test.wantEntries {
  70. t.Errorf("exportOptions(%s) = %d entries, want %d", test.expression, len(options), test.wantEntries)
  71. }
  72. })
  73. }
  74. }
  75. func TestOptionString(t *testing.T) {
  76. options := map[string]interface{}{
  77. "username": "tobychui",
  78. "padded": " spaced ",
  79. "number": 42.0,
  80. "nothing": nil,
  81. }
  82. tests := []struct {
  83. name string
  84. key string
  85. want string
  86. }{
  87. {name: "plain value", key: "username", want: "tobychui"},
  88. {name: "value is trimmed", key: "padded", want: "spaced"},
  89. {name: "wrong type", key: "number", want: ""},
  90. {name: "nil value", key: "nothing", want: ""},
  91. {name: "missing key", key: "absent", want: ""},
  92. }
  93. for _, test := range tests {
  94. t.Run(test.name, func(t *testing.T) {
  95. if got := optionString(options, test.key); got != test.want {
  96. t.Errorf("optionString(%q) = %q, want %q", test.key, got, test.want)
  97. }
  98. })
  99. }
  100. }
  101. func TestOptionBool(t *testing.T) {
  102. options := map[string]interface{}{
  103. "yes": true,
  104. "no": false,
  105. "textTrue": "true",
  106. "textUpper": "TRUE",
  107. "textFalse": "false",
  108. "number": 1.0,
  109. "nothing": nil,
  110. }
  111. tests := []struct {
  112. name string
  113. key string
  114. want bool
  115. }{
  116. {name: "boolean true", key: "yes", want: true},
  117. {name: "boolean false", key: "no", want: false},
  118. {name: "string true", key: "textTrue", want: true},
  119. {name: "string true uppercase", key: "textUpper", want: true},
  120. {name: "string false", key: "textFalse", want: false},
  121. {name: "number is not truthy", key: "number", want: false},
  122. {name: "nil value", key: "nothing", want: false},
  123. {name: "missing key", key: "absent", want: false},
  124. }
  125. for _, test := range tests {
  126. t.Run(test.name, func(t *testing.T) {
  127. if got := optionBool(options, test.key); got != test.want {
  128. t.Errorf("optionBool(%q) = %v, want %v", test.key, got, test.want)
  129. }
  130. })
  131. }
  132. }
  133. func TestOptionInt(t *testing.T) {
  134. options := map[string]interface{}{
  135. "float": 5.0,
  136. "integer": 7,
  137. "text": "12",
  138. "notANum": "abc",
  139. "nothing": nil,
  140. "boolean": true,
  141. "negative": -3.0,
  142. }
  143. tests := []struct {
  144. name string
  145. key string
  146. want int
  147. }{
  148. {name: "float from otto", key: "float", want: 5},
  149. {name: "native int", key: "integer", want: 7},
  150. {name: "numeric string", key: "text", want: 12},
  151. {name: "non numeric string", key: "notANum", want: 0},
  152. {name: "nil value", key: "nothing", want: 0},
  153. {name: "wrong type", key: "boolean", want: 0},
  154. {name: "negative number", key: "negative", want: -3},
  155. {name: "missing key", key: "absent", want: 0},
  156. }
  157. for _, test := range tests {
  158. t.Run(test.name, func(t *testing.T) {
  159. if got := optionInt(options, test.key); got != test.want {
  160. t.Errorf("optionInt(%q) = %d, want %d", test.key, got, test.want)
  161. }
  162. })
  163. }
  164. }
  165. func TestTrimVirtualPathSegments(t *testing.T) {
  166. tests := []struct {
  167. name string
  168. vpath string
  169. count int
  170. want string
  171. wantError bool
  172. }{
  173. {name: "nothing to trim", vpath: "user:/Code/QuickSend_PHP", count: 0, want: "user:/Code/QuickSend_PHP"},
  174. {name: "trim one level", vpath: "user:/Code/QuickSend_PHP/src", count: 1, want: "user:/Code/QuickSend_PHP"},
  175. {name: "trim two levels", vpath: "user:/Code/repo/a/b", count: 2, want: "user:/Code/repo"},
  176. {name: "trailing slash tolerated", vpath: "user:/Code/repo/src/", count: 1, want: "user:/Code/repo"},
  177. {name: "trim down to the storage root", vpath: "user:/repo", count: 1, want: "user:/"},
  178. {name: "storage root itself", vpath: "user:/", count: 0, want: "user:/"},
  179. {name: "uuid style root", vpath: "extuuid:/Projects/repo/src", count: 1, want: "extuuid:/Projects/repo"},
  180. {name: "trims past the root", vpath: "user:/repo", count: 2, wantError: true},
  181. {name: "not a virtual path", vpath: "/absolute/path", count: 1, wantError: true},
  182. }
  183. for _, test := range tests {
  184. t.Run(test.name, func(t *testing.T) {
  185. got, err := trimVirtualPathSegments(test.vpath, test.count)
  186. if test.wantError {
  187. if err == nil {
  188. t.Fatalf("trimVirtualPathSegments(%q, %d) = %q, want an error", test.vpath, test.count, got)
  189. }
  190. return
  191. }
  192. if err != nil {
  193. t.Fatalf("trimVirtualPathSegments(%q, %d) returned error: %v", test.vpath, test.count, err)
  194. }
  195. if got != test.want {
  196. t.Errorf("trimVirtualPathSegments(%q, %d) = %q, want %q", test.vpath, test.count, got, test.want)
  197. }
  198. })
  199. }
  200. }
  201. /*
  202. TestRepoRootVirtualPath covers the regression behind the doubled path bug:
  203. RepoRoot answers with an absolute OS path, and feeding that to the file system
  204. abstraction produced virtual paths like
  205. user:/D:/Github/arozos/src/files/users/TC/Code/QuickSend_PHP
  206. which then resolved to the storage root joined with the absolute path again.
  207. The mapping must be driven by the virtual path instead, and must never let an
  208. absolute real path leak into the result.
  209. */
  210. func TestRepoRootVirtualPath(t *testing.T) {
  211. //The real paths mirror what VirtualPathToRealPath produces on this project:
  212. //a path relative to the binary, under ./files/users/<user>/
  213. base := filepath.Join("files", "users", "TC")
  214. tests := []struct {
  215. name string
  216. vpath string
  217. rpath string
  218. realRoot string
  219. want string
  220. wantError bool
  221. }{
  222. {
  223. name: "path is the repository root",
  224. vpath: "user:/Code/QuickSend_PHP",
  225. rpath: filepath.Join(base, "Code", "QuickSend_PHP"),
  226. realRoot: absolutePath(t, filepath.Join(base, "Code", "QuickSend_PHP")),
  227. want: "user:/Code/QuickSend_PHP",
  228. },
  229. {
  230. name: "path is one level inside the repository",
  231. vpath: "user:/Code/QuickSend_PHP/src",
  232. rpath: filepath.Join(base, "Code", "QuickSend_PHP", "src"),
  233. realRoot: absolutePath(t, filepath.Join(base, "Code", "QuickSend_PHP")),
  234. want: "user:/Code/QuickSend_PHP",
  235. },
  236. {
  237. name: "path is deep inside the repository",
  238. vpath: "user:/Code/QuickSend_PHP/src/mod/git",
  239. rpath: filepath.Join(base, "Code", "QuickSend_PHP", "src", "mod", "git"),
  240. realRoot: absolutePath(t, filepath.Join(base, "Code", "QuickSend_PHP")),
  241. want: "user:/Code/QuickSend_PHP",
  242. },
  243. {
  244. name: "repository directly under the storage root",
  245. vpath: "user:/QuickSend_PHP/src",
  246. rpath: filepath.Join(base, "QuickSend_PHP", "src"),
  247. realRoot: absolutePath(t, filepath.Join(base, "QuickSend_PHP")),
  248. want: "user:/QuickSend_PHP",
  249. },
  250. {
  251. name: "root is not an ancestor",
  252. vpath: "user:/Code/QuickSend_PHP",
  253. rpath: filepath.Join(base, "Code", "QuickSend_PHP"),
  254. realRoot: absolutePath(t, filepath.Join(base, "Elsewhere")),
  255. wantError: true,
  256. },
  257. }
  258. for _, test := range tests {
  259. t.Run(test.name, func(t *testing.T) {
  260. got, err := repoRootVirtualPath(test.vpath, test.rpath, test.realRoot)
  261. if test.wantError {
  262. if err == nil {
  263. t.Fatalf("repoRootVirtualPath() = %q, want an error", got)
  264. }
  265. return
  266. }
  267. if err != nil {
  268. t.Fatalf("repoRootVirtualPath() returned error: %v", err)
  269. }
  270. if got != test.want {
  271. t.Errorf("repoRootVirtualPath() = %q, want %q", got, test.want)
  272. }
  273. //No absolute real path may ever appear in a virtual path
  274. if strings.Contains(got, ":\\") || strings.Contains(got, "files/users") {
  275. t.Errorf("repoRootVirtualPath() leaked a real path into %q", got)
  276. }
  277. if strings.Count(got, ":/") != 1 {
  278. t.Errorf("repoRootVirtualPath() = %q, want exactly one virtual root marker", got)
  279. }
  280. })
  281. }
  282. }
  283. // absolutePath is the helper the RepoRootVirtualPath table uses to build the
  284. // absolute roots RepoRoot would return.
  285. func absolutePath(t *testing.T, path string) string {
  286. t.Helper()
  287. absolute, err := filepath.Abs(path)
  288. if err != nil {
  289. t.Fatalf("cannot absolutise %s: %v", path, err)
  290. }
  291. return filepath.ToSlash(absolute)
  292. }
  293. func TestCheckFshSupportsGit(t *testing.T) {
  294. tests := []struct {
  295. name string
  296. fsh *filesystem.FileSystemHandler
  297. wantError bool
  298. }{
  299. {
  300. name: "local read write pool",
  301. fsh: &filesystem.FileSystemHandler{Name: "User", RequireBuffer: false, ReadOnly: false},
  302. },
  303. {
  304. name: "nil handler",
  305. fsh: nil,
  306. wantError: true,
  307. },
  308. {
  309. name: "network backed pool",
  310. fsh: &filesystem.FileSystemHandler{Name: "WebDAV Drive", RequireBuffer: true},
  311. wantError: true,
  312. },
  313. {
  314. name: "read only pool",
  315. fsh: &filesystem.FileSystemHandler{Name: "Backup", ReadOnly: true},
  316. wantError: true,
  317. },
  318. }
  319. for _, test := range tests {
  320. t.Run(test.name, func(t *testing.T) {
  321. err := checkFshSupportsGit(test.fsh)
  322. if test.wantError && err == nil {
  323. t.Errorf("checkFshSupportsGit() = nil, want an error")
  324. }
  325. if !test.wantError && err != nil {
  326. t.Errorf("checkFshSupportsGit() = %v, want nil", err)
  327. }
  328. })
  329. }
  330. }
  331. func TestGitOperationEnvelopes(t *testing.T) {
  332. vm := otto.New()
  333. tests := []struct {
  334. name string
  335. value otto.Value
  336. wantSuccess bool
  337. wantAuthRequired bool
  338. wantMessage string
  339. wantError string
  340. }{
  341. {
  342. name: "success envelope",
  343. value: gitOperationSuccess(vm, "pushed to origin/master"),
  344. wantSuccess: true,
  345. wantMessage: "pushed to origin/master",
  346. },
  347. {
  348. name: "plain failure",
  349. value: gitOperationFailure(vm, errors.New("connection refused")),
  350. wantError: "connection refused",
  351. },
  352. {
  353. name: "authentication failure is flagged",
  354. value: gitOperationFailure(vm, git.ErrAuthRequired),
  355. wantAuthRequired: true,
  356. wantError: git.ErrAuthRequired.Error(),
  357. },
  358. }
  359. for _, test := range tests {
  360. t.Run(test.name, func(t *testing.T) {
  361. raw, err := test.value.ToString()
  362. if err != nil {
  363. t.Fatalf("cannot read the returned value: %v", err)
  364. }
  365. result := git.OperationResult{}
  366. if err := json.Unmarshal([]byte(raw), &result); err != nil {
  367. t.Fatalf("returned value is not valid JSON (%s): %v", raw, err)
  368. }
  369. if result.Success != test.wantSuccess {
  370. t.Errorf("Success = %v, want %v", result.Success, test.wantSuccess)
  371. }
  372. if result.AuthRequired != test.wantAuthRequired {
  373. t.Errorf("AuthRequired = %v, want %v", result.AuthRequired, test.wantAuthRequired)
  374. }
  375. if result.Message != test.wantMessage {
  376. t.Errorf("Message = %q, want %q", result.Message, test.wantMessage)
  377. }
  378. if result.Error != test.wantError {
  379. t.Errorf("Error = %q, want %q", result.Error, test.wantError)
  380. }
  381. })
  382. }
  383. }
  384. func TestGitJSONErrorFlagsAuthRequired(t *testing.T) {
  385. vm := otto.New()
  386. tests := []struct {
  387. name string
  388. err error
  389. wantAuthRequired bool
  390. }{
  391. {name: "plain error", err: errors.New("not a git repository"), wantAuthRequired: false},
  392. {name: "auth error", err: git.ErrAuthRequired, wantAuthRequired: true},
  393. }
  394. for _, test := range tests {
  395. t.Run(test.name, func(t *testing.T) {
  396. raw, err := gitJSONError(vm, test.err).ToString()
  397. if err != nil {
  398. t.Fatalf("cannot read the returned value: %v", err)
  399. }
  400. payload := map[string]interface{}{}
  401. if err := json.Unmarshal([]byte(raw), &payload); err != nil {
  402. t.Fatalf("returned value is not valid JSON (%s): %v", raw, err)
  403. }
  404. if payload["error"] != test.err.Error() {
  405. t.Errorf("error = %v, want %q", payload["error"], test.err.Error())
  406. }
  407. _, flagged := payload["authRequired"]
  408. if flagged != test.wantAuthRequired {
  409. t.Errorf("authRequired present = %v, want %v", flagged, test.wantAuthRequired)
  410. }
  411. })
  412. }
  413. }
  414. func TestGitJSONValueIsParseableInTheVM(t *testing.T) {
  415. vm := otto.New()
  416. status := &git.RepoStatus{
  417. Branch: "master",
  418. Clean: false,
  419. Changes: []git.FileChange{{Path: "a.txt", Status: "modified"}},
  420. }
  421. value := gitJSONValue(vm, status)
  422. raw, err := value.ToString()
  423. if err != nil {
  424. t.Fatalf("cannot read the returned value: %v", err)
  425. }
  426. //The JS wrapper JSON.parses whatever the native call returns, so make sure
  427. //the round trip works inside the VM itself.
  428. if err := vm.Set("_payload", raw); err != nil {
  429. t.Fatalf("cannot inject the payload: %v", err)
  430. }
  431. result, err := vm.Run(`JSON.parse(_payload).changes[0].path`)
  432. if err != nil {
  433. t.Fatalf("the payload is not parseable in the VM: %v", err)
  434. }
  435. parsed, _ := result.ToString()
  436. if parsed != "a.txt" {
  437. t.Errorf("parsed path = %q, want %q", parsed, "a.txt")
  438. }
  439. }
  440. // TestGitLibJavaScriptWrapper runs the in-VM wrapper against stub natives, so a
  441. // syntax error or a typo in a function name fails the build instead of a user's
  442. // script at runtime.
  443. func TestGitLibJavaScriptWrapper(t *testing.T) {
  444. vm := otto.New()
  445. //Every native the wrapper references, echoing back a JSON payload that
  446. //names the function that was called.
  447. natives := []string{
  448. "_git_isrepo", "_git_reporoot", "_git_remotehost", "_git_hascredential",
  449. "_git_init", "_git_clone", "_git_status", "_git_log", "_git_branches",
  450. "_git_checkout", "_git_remotes", "_git_addremote", "_git_removeremote",
  451. "_git_add", "_git_addall", "_git_unstage", "_git_discard", "_git_commit",
  452. "_git_diff", "_git_diffcommit", "_git_commitfiles", "_git_fetch",
  453. "_git_pull", "_git_push", "_git_savecredential", "_git_listcredentials",
  454. "_git_removecredential", "_git_ignore", "_git_fileblob",
  455. "_git_checkoutcommit", "_git_resettocommit", "_git_createbranchat",
  456. "_git_createtag", "_git_revertcommit", "_git_cherrypickcommit", "_git_amendmessage",
  457. "_git_deletebranch", "_git_renamebranch", "_git_deleteremotebranch", "_git_renameremotebranch",
  458. }
  459. for _, native := range natives {
  460. name := native
  461. if err := vm.Set(name, func(call otto.FunctionCall) otto.Value {
  462. value, _ := vm.ToValue(`{"success":true,"message":"` + name + `"}`)
  463. return value
  464. }); err != nil {
  465. t.Fatalf("cannot inject %s: %v", name, err)
  466. }
  467. }
  468. if _, err := vm.Run(gitLibJavaScript); err != nil {
  469. t.Fatalf("the git library wrapper does not evaluate: %v", err)
  470. }
  471. //Each wrapped call must reach its native and hand back a parsed object
  472. calls := []struct {
  473. name string
  474. expression string
  475. }{
  476. {name: "init", expression: `git.init("user:/repo").message`},
  477. {name: "clone", expression: `git.clone("https://example.com/a.git", "user:/repo").message`},
  478. {name: "status", expression: `git.status("user:/repo").message`},
  479. {name: "log", expression: `git.log("user:/repo", 10).message`},
  480. {name: "branches", expression: `git.branches("user:/repo").message`},
  481. {name: "checkout", expression: `git.checkout("user:/repo", "master").message`},
  482. {name: "remotes", expression: `git.remotes("user:/repo").message`},
  483. {name: "addRemote", expression: `git.addRemote("user:/repo", "origin", "url").message`},
  484. {name: "removeRemote", expression: `git.removeRemote("user:/repo", "origin").message`},
  485. {name: "add", expression: `git.add("user:/repo", ["a.txt"]).message`},
  486. {name: "addAll", expression: `git.addAll("user:/repo").message`},
  487. {name: "unstage", expression: `git.unstage("user:/repo", ["a.txt"]).message`},
  488. {name: "discard", expression: `git.discard("user:/repo", ["a.txt"]).message`},
  489. {name: "commit", expression: `git.commit("user:/repo", "message", ["a.txt"]).message`},
  490. {name: "ignore", expression: `git.ignore("user:/repo", ["*.log"]).message`},
  491. {name: "diff", expression: `git.diff("user:/repo", "a.txt").message`},
  492. {name: "diffCommit", expression: `git.diffCommit("user:/repo", "abc", "a.txt").message`},
  493. {name: "commitFiles", expression: `git.commitFiles("user:/repo", "abc").message`},
  494. {name: "deleteBranch", expression: `git.deleteBranch("user:/repo", "old", true).message`},
  495. {name: "renameBranch", expression: `git.renameBranch("user:/repo", "old", "new").message`},
  496. {name: "deleteRemoteBranch", expression: `git.deleteRemoteBranch("user:/repo", "origin", "old").message`},
  497. {name: "renameRemoteBranch", expression: `git.renameRemoteBranch("user:/repo", "origin", "old", "new").message`},
  498. {name: "checkoutCommit", expression: `git.checkoutCommit("user:/repo", "abc").message`},
  499. {name: "resetToCommit", expression: `git.resetToCommit("user:/repo", "abc", "hard").message`},
  500. {name: "createBranchAt", expression: `git.createBranchAt("user:/repo", "legacy", "abc").message`},
  501. {name: "createTag", expression: `git.createTag("user:/repo", "v1", "abc", "msg").message`},
  502. {name: "revertCommit", expression: `git.revertCommit("user:/repo", "abc").message`},
  503. {name: "cherryPickCommit", expression: `git.cherryPickCommit("user:/repo", "abc").message`},
  504. {name: "amendMessage", expression: `git.amendMessage("user:/repo", "new msg").message`},
  505. {name: "fetch", expression: `git.fetch("user:/repo").message`},
  506. {name: "pull", expression: `git.pull("user:/repo").message`},
  507. {name: "push", expression: `git.push("user:/repo").message`},
  508. {name: "saveCredential", expression: `git.saveCredential("github.com", "u", "t").message`},
  509. {name: "listCredentials", expression: `git.listCredentials().message`},
  510. {name: "removeCredential", expression: `git.removeCredential("github.com").message`},
  511. }
  512. for _, call := range calls {
  513. t.Run(call.name, func(t *testing.T) {
  514. value, err := vm.Run(call.expression)
  515. if err != nil {
  516. t.Fatalf("%s failed to evaluate: %v", call.expression, err)
  517. }
  518. got, _ := value.ToString()
  519. want := "_git_" + strings.ToLower(call.name)
  520. if got != want {
  521. t.Errorf("%s reached %q, want %q", call.name, got, want)
  522. }
  523. })
  524. }
  525. }
  526. func TestGitLibJavaScriptHandlesMalformedNativeResponse(t *testing.T) {
  527. vm := otto.New()
  528. //A native that fails returns false; the wrapper must turn that into an
  529. //error object rather than throwing inside the user's script.
  530. if err := vm.Set("_git_status", func(call otto.FunctionCall) otto.Value {
  531. return otto.FalseValue()
  532. }); err != nil {
  533. t.Fatalf("cannot inject the stub: %v", err)
  534. }
  535. if err := vm.Set("_git_log", func(call otto.FunctionCall) otto.Value {
  536. value, _ := vm.ToValue("this is not json")
  537. return value
  538. }); err != nil {
  539. t.Fatalf("cannot inject the stub: %v", err)
  540. }
  541. for _, name := range []string{"_git_isrepo", "_git_reporoot", "_git_remotehost", "_git_hascredential",
  542. "_git_init", "_git_clone", "_git_branches", "_git_checkout", "_git_remotes",
  543. "_git_addremote", "_git_removeremote", "_git_add", "_git_addall", "_git_unstage",
  544. "_git_discard", "_git_commit", "_git_ignore", "_git_diff", "_git_diffcommit", "_git_commitfiles",
  545. "_git_fetch", "_git_pull", "_git_push", "_git_savecredential", "_git_listcredentials",
  546. "_git_removecredential", "_git_deletebranch", "_git_renamebranch",
  547. "_git_deleteremotebranch", "_git_renameremotebranch"} {
  548. vm.Set(name, func(call otto.FunctionCall) otto.Value { return otto.FalseValue() })
  549. }
  550. if _, err := vm.Run(gitLibJavaScript); err != nil {
  551. t.Fatalf("the git library wrapper does not evaluate: %v", err)
  552. }
  553. tests := []struct {
  554. name string
  555. expression string
  556. }{
  557. {name: "native returned false", expression: `git.status("user:/repo").error`},
  558. {name: "native returned garbage", expression: `git.log("user:/repo").error`},
  559. }
  560. for _, test := range tests {
  561. t.Run(test.name, func(t *testing.T) {
  562. value, err := vm.Run(test.expression)
  563. if err != nil {
  564. t.Fatalf("%s failed to evaluate: %v", test.expression, err)
  565. }
  566. message, _ := value.ToString()
  567. if message == "" || message == "undefined" {
  568. t.Errorf("%s = %q, want a populated error message", test.expression, message)
  569. }
  570. })
  571. }
  572. }
  573. func TestGitCredentialStoreRequiresManager(t *testing.T) {
  574. gateway := &Gateway{Option: &AgiSysInfo{}}
  575. if _, err := gateway.gitCredentialStore(); err == nil {
  576. t.Errorf("gitCredentialStore() without a manager = nil error, want an error")
  577. }
  578. }
  579. func TestResolveGitCredentialPrefersExplicitOptions(t *testing.T) {
  580. gateway := &Gateway{Option: &AgiSysInfo{}}
  581. username, token := gateway.resolveGitCredential(nil, "https://github.com/a/b.git", map[string]interface{}{
  582. "username": "tobychui",
  583. "token": "ghp_explicit",
  584. })
  585. if username != "tobychui" || token != "ghp_explicit" {
  586. t.Errorf("resolveGitCredential() = (%q, %q), want the values passed in the options", username, token)
  587. }
  588. }