11package main
22
33import (
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
4244func (os * FakeOS ) Exit (code int ) {
@@ -73,6 +75,11 @@ func (os *FakeOS) ReadDir(path string) ([]realos.DirEntry, error) {
7375func (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