Skip to content

Commit e73ecde

Browse files
Show unified diff before auto-saving in switch-target
Add showDiff call when switch-target auto-saves a named current target, displaying what changed since last save. Add tests for showDiff, createBuildMeta, and createSemVer functions.
1 parent 8d03762 commit e73ecde

2 files changed

Lines changed: 166 additions & 1 deletion

File tree

cf_targets.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,8 @@ func (c *TargetsPlugin) SwitchTargetCommand(args []string) {
426426

427427
if !*force && c.status.currentNeedsSaving {
428428
if c.status.currentHasName {
429+
// Show what changed before auto-saving
430+
c.showDiff(c.configPath)
429431
// Auto-save the named current target
430432
savePath := c.targetPath(c.status.currentName)
431433
c.copyContents(c.configPath, savePath)

cf_targets_test.go

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"errors"
56
realos "os"
67
"path/filepath"
@@ -37,6 +38,7 @@ type FakeOS struct {
3738
readlineCalled int
3839
readlineShouldReturn string
3940
readlineShouldReturnError error
41+
readfileShouldReturnMap map[string][]byte
4042
}
4143

4244
func (os *FakeOS) Exit(code int) {
@@ -73,6 +75,11 @@ func (os *FakeOS) ReadDir(path string) ([]realos.DirEntry, error) {
7375
func (os *FakeOS) ReadFile(path string) ([]byte, error) {
7476
os.readfileCalled++
7577
os.readfileCalledWithPath = path
78+
if os.readfileShouldReturnMap != nil {
79+
if data, ok := os.readfileShouldReturnMap[path]; ok {
80+
return data, nil
81+
}
82+
}
7683
return os.readfileShouldReturn, nil
7784
}
7885

@@ -308,11 +315,25 @@ var _ = Describe("TargetsPlugin", func() {
308315
Expect(output).To(ContainSubstrings([]string{"Set target to", "dest"}))
309316
})
310317

311-
It("auto-saves named current target before switching", func() {
318+
It("auto-saves named current target and shows diff before switching", func() {
312319
targetFile := filepath.Join(tmpDir, "dest"+targetsPlugin.suffix)
313320
err := realos.WriteFile(targetFile, []byte("{}"), 0600)
314321
Expect(err).NotTo(HaveOccurred())
315322

323+
// Provide differing JSON for showDiff: currentPath (saved) vs configPath (live)
324+
savedJSON, _ := json.MarshalIndent(map[string]interface{}{
325+
"AccessToken": "tok", "RefreshToken": "ref", "UAAOAuthClientSecret": "sec",
326+
"ColorEnabled": "true",
327+
}, "", " ")
328+
liveJSON, _ := json.MarshalIndent(map[string]interface{}{
329+
"AccessToken": "tok", "RefreshToken": "ref", "UAAOAuthClientSecret": "sec",
330+
"ColorEnabled": "false",
331+
}, "", " ")
332+
fakeOS.readfileShouldReturnMap = map[string][]byte{
333+
targetsPlugin.currentPath: savedJSON,
334+
targetsPlugin.configPath: liveJSON,
335+
}
336+
316337
// Simulate named current target with unsaved changes
317338
targetsPlugin.status = TargetStatus{true, "origin", true, false}
318339

@@ -321,6 +342,9 @@ var _ = Describe("TargetsPlugin", func() {
321342
})
322343
Expect(fakeOS.exitCalled).To(Equal(0))
323344
Expect(fakeOS.writefileCalled).To(Equal(2)) // save + switch
345+
// Diff should appear before the save message
346+
Expect(output).To(ContainSubstrings([]string{"---", "Current"}))
347+
Expect(output).To(ContainSubstrings([]string{"+++", "Target"}))
324348
Expect(output).To(ContainSubstrings([]string{"Saved current target as", "origin"}))
325349
Expect(output).To(ContainSubstrings([]string{"Set target to", "dest"}))
326350
})
@@ -440,6 +464,145 @@ var _ = Describe("TargetsPlugin", func() {
440464
})
441465
})
442466

467+
Describe("createBuildMeta", func() {
468+
It("returns os.arch when no build metadata", func() {
469+
Expect(createBuildMeta("darwin", "arm64", "")).To(Equal("darwin.arm64"))
470+
})
471+
472+
It("returns os.arch.build when build metadata provided", func() {
473+
Expect(createBuildMeta("linux", "amd64", "ci")).To(Equal("linux.amd64.ci"))
474+
})
475+
476+
It("trims whitespace from all parts", func() {
477+
Expect(createBuildMeta(" darwin ", " arm64 ", " ci ")).To(Equal("darwin.arm64.ci"))
478+
})
479+
480+
It("panics when os is empty", func() {
481+
Expect(func() { createBuildMeta("", "arm64", "") }).To(PanicWith(ContainSubstring("Go meta data is missing")))
482+
})
483+
484+
It("panics when arch is empty", func() {
485+
Expect(func() { createBuildMeta("darwin", "", "") }).To(PanicWith(ContainSubstring("Go meta data is missing")))
486+
})
487+
488+
It("panics when os is only whitespace", func() {
489+
Expect(func() { createBuildMeta(" ", "arm64", "") }).To(PanicWith(ContainSubstring("Go meta data is missing")))
490+
})
491+
})
492+
493+
Describe("createSemVer", func() {
494+
It("returns major.minor.patch", func() {
495+
Expect(createSemVer("1", "2", "3", "", "")).To(Equal("1.2.3"))
496+
})
497+
498+
It("appends prerelease with hyphen", func() {
499+
Expect(createSemVer("1", "2", "3", "beta", "")).To(Equal("1.2.3-beta"))
500+
})
501+
502+
It("appends build with plus", func() {
503+
Expect(createSemVer("1", "2", "3", "", "linux.amd64")).To(Equal("1.2.3+linux.amd64"))
504+
})
505+
506+
It("appends both prerelease and build", func() {
507+
Expect(createSemVer("1", "2", "3", "dev", "darwin.arm64")).To(Equal("1.2.3-dev+darwin.arm64"))
508+
})
509+
510+
It("trims whitespace from all parts", func() {
511+
Expect(createSemVer(" 1 ", " 2 ", " 3 ", " rc1 ", " meta ")).To(Equal("1.2.3-rc1+meta"))
512+
})
513+
514+
It("panics when major is empty", func() {
515+
Expect(func() { createSemVer("", "2", "3", "", "") }).To(PanicWith(ContainSubstring("Semanic version is missing")))
516+
})
517+
518+
It("panics when minor is empty", func() {
519+
Expect(func() { createSemVer("1", "", "3", "", "") }).To(PanicWith(ContainSubstring("Semanic version is missing")))
520+
})
521+
522+
It("panics when patch is empty", func() {
523+
Expect(func() { createSemVer("1", "2", "", "", "") }).To(PanicWith(ContainSubstring("Semanic version is missing")))
524+
})
525+
})
526+
527+
Describe("showDiff", func() {
528+
makeJSON := func(overrides map[string]interface{}) []byte {
529+
base := map[string]interface{}{
530+
"AccessToken": "token-abc",
531+
"RefreshToken": "refresh-xyz",
532+
"UAAOAuthClientSecret": "secret-123",
533+
"Target": "https://api.example.com",
534+
"ColorEnabled": "true",
535+
}
536+
for k, v := range overrides {
537+
base[k] = v
538+
}
539+
data, err := json.MarshalIndent(base, "", " ")
540+
Expect(err).NotTo(HaveOccurred())
541+
return data
542+
}
543+
544+
It("prints unified diff when files differ", func() {
545+
currentJSON := makeJSON(map[string]interface{}{"ColorEnabled": "true"})
546+
targetJSON := makeJSON(map[string]interface{}{"ColorEnabled": "false"})
547+
548+
fakeOS.readfileShouldReturnMap = map[string][]byte{
549+
targetsPlugin.currentPath: currentJSON,
550+
targetsPlugin.targetPath("other"): targetJSON,
551+
}
552+
553+
output := CaptureOutput(func() {
554+
targetsPlugin.showDiff(targetsPlugin.targetPath("other"))
555+
})
556+
Expect(output).To(ContainSubstrings([]string{"---", "Current"}))
557+
Expect(output).To(ContainSubstrings([]string{"+++", "Target"}))
558+
Expect(output).To(ContainSubstrings([]string{`"true"`}))
559+
Expect(output).To(ContainSubstrings([]string{`"false"`}))
560+
})
561+
562+
It("prints no differences when files are identical", func() {
563+
jsonData := makeJSON(nil)
564+
565+
fakeOS.readfileShouldReturnMap = map[string][]byte{
566+
targetsPlugin.currentPath: jsonData,
567+
targetsPlugin.targetPath("same"): jsonData,
568+
}
569+
570+
output := CaptureOutput(func() {
571+
targetsPlugin.showDiff(targetsPlugin.targetPath("same"))
572+
})
573+
Expect(output).To(ContainSubstrings([]string{"hmmm no differences"}))
574+
})
575+
576+
It("redacts sensitive fields in diff output", func() {
577+
currentJSON := makeJSON(map[string]interface{}{
578+
"AccessToken": "current-token",
579+
"RefreshToken": "current-refresh",
580+
})
581+
targetJSON := makeJSON(map[string]interface{}{
582+
"AccessToken": "different-token",
583+
"RefreshToken": "different-refresh",
584+
})
585+
586+
fakeOS.readfileShouldReturnMap = map[string][]byte{
587+
targetsPlugin.currentPath: currentJSON,
588+
targetsPlugin.targetPath("redacted"): targetJSON,
589+
}
590+
591+
output := CaptureOutput(func() {
592+
targetsPlugin.showDiff(targetsPlugin.targetPath("redacted"))
593+
})
594+
// Tokens should be redacted
595+
Expect(output).To(ContainSubstrings([]string{"REDACTED sha256("}))
596+
// Raw tokens should NOT appear
597+
for _, line := range output {
598+
Expect(line).NotTo(ContainSubstring("current-token"))
599+
Expect(line).NotTo(ContainSubstring("different-token"))
600+
Expect(line).NotTo(ContainSubstring("current-refresh"))
601+
Expect(line).NotTo(ContainSubstring("different-refresh"))
602+
}
603+
})
604+
})
605+
443606
Describe("Configuration File Manipulation", func() {
444607

445608
It("creates the proper target directory", func() {

0 commit comments

Comments
 (0)