package emulator import ( "context" "fmt" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "edge-infra.dev/pkg/sds/emergencyaccess/msgdata" ) // Mock CLIService var ( connectC = make(chan bool) ) type cliService struct { topicTemplate string subsciptionTemplate string sentMessage string idleTime time.Time darkmode bool enablePerSessionSubscription *bool CLIService } func (cls cliService) Connect(context.Context, string, string, string, string) error { // set the test value in our test channel connectC <- true // pausing the thread here to stop session prompt being called. time.Sleep(10 * time.Second) return nil } func (cls *cliService) Send(command string) (string, error) { if command == "a bad command" { return "", fmt.Errorf("a bad command was sent") } cls.sentMessage = command return "abcd", nil } func (cls cliService) End() error { return nil } func (cls *cliService) SetTopicTemplate(topicTemplate string) { cls.topicTemplate = topicTemplate } func (cls *cliService) SetSubscriptionTemplate(subscriptionTemplate string) { cls.subsciptionTemplate = subscriptionTemplate } func (cls cliService) GetDisplayChannel() <-chan msgdata.CommandResponse { return make(<-chan msgdata.CommandResponse) } func (cls cliService) GetErrorChannel() <-chan error { return make(<-chan error) } func (cls cliService) GetSessionContext() context.Context { return context.Background() } func (cls cliService) RetrieveIdentity(_ context.Context) error { return nil } func (cls cliService) UserID() string { return "" } func createTempFile(t *testing.T) (filepath string) { d := t.TempDir() f, err := os.CreateTemp(d, "") assert.NoError(t, err) return f.Name() } func (cls cliService) IdleTime() time.Duration { return time.Since(cls.idleTime) } func (cls *cliService) SetDarkmode(val bool) { cls.darkmode = val } func (cls cliService) Darkmode() bool { return cls.darkmode } func (cls *cliService) EnablePerSessionSubscription() { b := true cls.enablePerSessionSubscription = &b } func (cls *cliService) DisablePerSessionSubscription() { b := false cls.enablePerSessionSubscription = &b } func TestSuccessParseConnectInput(t *testing.T) { cases := map[string]string{ "Standard prompt": "connect sessionID bannerID storeID terminalID", "Extra space in prompt": "connect sessionID bannerID storeID terminalID", "Trailing newline in prompt": "connect sessionID bannerID storeID terminalID\n", } expVal := connectionData{ projectID: "sessionID", bannerID: "bannerID", storeID: "storeID", terminalID: "terminalID", } em := Emulator{} for name, prompt := range cases { t.Run(name, func(t *testing.T) { res, err := em.parseConnectInput(prompt) assert.NoError(t, err) assert.Equal(t, res, expVal) }) } } func TestFailParseConnectInputWithWorkspace(t *testing.T) { type caseStruct struct { name string input string workspace workspace expErr error } tests := []caseStruct{ { name: "No parameters", input: "connect", workspace: workspace{}, expErr: fmt.Errorf("wrong number of values given"), }, { name: "No bannerID", input: "connect a-terminal-id", workspace: workspace{}, // emulator shouldn't error when project ID is not set expErr: fmt.Errorf("bannerID in workspace was not set"), }, { name: "No projectID and two params", input: "connect a-store-id a-terminal-id", // emulator shouldn't error when project ID is not set expErr: fmt.Errorf("bannerID in workspace was not set"), }, { name: "No projectID and three params", input: "connect a-banner-id a-store-id a-terminal-id", // cliServiceItfc should return an error if project ID is required expErr: nil, }, { name: "No bannerID", input: "connect a-terminal-id", workspace: workspace{ProjectID: "a-project-id"}, expErr: fmt.Errorf("bannerID in workspace was not set"), }, { name: "No bannerID and two params", input: "connect a-store-id a-terminal-id", workspace: workspace{ProjectID: "a-project-id"}, expErr: fmt.Errorf("bannerID in workspace was not set"), }, { name: "No storeID", input: "connect a-terminal-id", workspace: workspace{ProjectID: "a-project-id", BannerID: "a-banner-id"}, expErr: fmt.Errorf("storeID in workspace was not set"), }, } for i, test := range tests { em := Emulator{} em.workspace = &tests[i].workspace t.Run(test.name, func(t *testing.T) { _, err := em.parseConnectInput(test.input) assert.Equal(t, test.expErr, err) }) } } func TestSuccessParseInputWithWorkspace(t *testing.T) { type caseStruct struct { name string input string workspace workspace } expOutput := connectionData{ projectID: "a-project-ID", bannerID: "a-banner-ID", storeID: "a-store-ID", terminalID: "a-terminal-ID", } tests := []caseStruct{ { name: "Connect with all params", input: "connect a-project-ID a-banner-ID a-store-ID a-terminal-ID", workspace: workspace{}, }, { name: "Connect one param", input: "connect a-terminal-ID", workspace: workspace{ProjectID: "a-project-ID", BannerID: "a-banner-ID", StoreID: "a-store-ID"}, }, { name: "Connect with two params", input: "connect a-store-ID a-terminal-ID", workspace: workspace{ProjectID: "a-project-ID", BannerID: "a-banner-ID"}, }, { name: "Connect with three params", input: "connect a-banner-ID a-store-ID a-terminal-ID", workspace: workspace{ProjectID: "a-project-ID"}, }, } for i, test := range tests { em := Emulator{} em.workspace = &tests[i].workspace t.Run(test.name, func(t *testing.T) { res, err := em.parseConnectInput(test.input) assert.Nil(t, err) assert.Equal(t, res, expOutput) }) } } func TestRcliConfig(t *testing.T) { t.Parallel() ttrue := true ffalse := false tests := map[string]struct { command string err assert.ErrorAssertionFunc expCls cliService expWs workspace }{ "Disable Session Subscription": { command: "rcliconfig disablePerSessionSubscription", err: assert.NoError, expCls: cliService{ enablePerSessionSubscription: &ffalse, }, expWs: workspace{ DisablePerSessionSubscription: true, }, }, "Enable Session Subscription": { command: "rcliconfig enablePerSessionSubscription", err: assert.NoError, expCls: cliService{ enablePerSessionSubscription: &ttrue, }, expWs: workspace{ DisablePerSessionSubscription: false, }, }, "Successful topicTemplate": { command: "rcliconfig topicTemplate a-template", err: assert.NoError, expCls: cliService{ topicTemplate: "a-template", }, }, "Successful subsciptionTemplate": { command: "rcliconfig subscriptionTemplate a-template", err: assert.NoError, expCls: cliService{ subsciptionTemplate: "a-template", }, }, "subsciptionTemplate too few argument": { command: "rcliconfig subscriptionTemplate", err: EqualError("Unexpected number of arguments for subcommand subscriptionTemplate"), }, "subsciptionTemplate too many arguments": { command: "rcliconfig subscriptionTemplate a b", err: EqualError("Unexpected number of arguments for subcommand subscriptionTemplate"), }, "unknown command": { command: "rcliconfig not-a-command another-parameter", err: EqualError(ErrorRCLIUnknownSubcmd.Error()), }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() ws := workspace{} mcls := cliService{} ctx := context.Background() config := NewConfig(ctx, t.TempDir()) em := NewEmulator(ctx, &mcls, config) em.workspace = &ws err := em.rcliconfig(tc.command) tc.err(t, err) assert.Equal(t, tc.expCls, mcls) assert.Equal(t, tc.expWs, ws) }) } } func TestSuccessConnectionFromExecutor(t *testing.T) { // mocking mcls := cliService{} ctx := context.Background() config := NewConfig(ctx, t.TempDir()) em := NewEmulator(ctx, &mcls, config) em.runCtx = ctx filepath := createTempFile(t) ch, err := newCommandHistory(filepath) assert.NoError(t, err) em.connectHistory = ch // test go func() { em.connectPromptExecutor("connect a b c d") }() val := <-connectC assert.True(t, val) } func TestSuccessDarkConnectionFromExecutor(t *testing.T) { // mocking mcls := cliService{} ctx := context.Background() config := NewConfig(ctx, t.TempDir()) em := NewEmulator(ctx, &mcls, config) em.runCtx = ctx filepath := createTempFile(t) ch, err := newCommandHistory(filepath) assert.NoError(t, err) em.connectHistory = ch // test go func() { em.connectPromptExecutor("dark connect a b c d") }() val := <-connectC assert.True(t, val) assert.True(t, mcls.darkmode) } func TestSuccessRcliConfFromExecutor(t *testing.T) { // mocking mcls := cliService{} ctx := context.Background() config := NewConfig(ctx, t.TempDir()) em := NewEmulator(ctx, &mcls, config) em.runCtx = ctx filepath := createTempFile(t) ch, err := newCommandHistory(filepath) assert.NoError(t, err) em.connectHistory = ch // test em.connectPromptExecutor("rcliconfig subscriptionTemplate a-sub-template") assert.Equal(t, mcls.subsciptionTemplate, "a-sub-template") em.connectPromptExecutor("rcliconfig topicTemplate a-top-template") assert.Equal(t, mcls.topicTemplate, "a-top-template") } func TestSessionPromptExecutor(t *testing.T) { // mocking mcls := cliService{} ctx := context.Background() config := NewConfig(ctx, t.TempDir()) em := NewEmulator(ctx, &mcls, config) filepath := createTempFile(t) ch, err := newCommandHistory(filepath) assert.NoError(t, err) em.sessionHistory = ch // run display routine to stop executor from blocking go em.display(ctx) type caseStruct struct { name string command string expectedOutput string } tests := []caseStruct{ { name: "Send", command: "a command", expectedOutput: "a command", }, { name: "Exit with q", command: "q", expectedOutput: "", }, { name: "Exit with exit", command: "exit", expectedOutput: "", }, { name: "Exit with end", command: "end", expectedOutput: "", }, { name: "Help", command: "help", expectedOutput: "", }, } //tests for _, test := range tests { t.Run(test.name, func(t *testing.T) { em.sessionPromptExecutor(test.command) assert.Nil(t, err) assert.Equal(t, test.expectedOutput, mcls.sentMessage) mcls.sentMessage = "" em.unPauseSessionPrompt() // TODO: Confirm correct values and keys? assert.Len(t, em.commandOptions, 1) }) } } func TestReturnOnEmptyMessage(t *testing.T) { // mocking mcls := cliService{} ctx := context.Background() config := NewConfig(ctx, t.TempDir()) em := NewEmulator(ctx, &mcls, config) filepath := createTempFile(t) ch, err := newCommandHistory(filepath) assert.NoError(t, err) em.sessionHistory = ch // run display routine to stop executor from blocking go em.display(ctx) // test em.sessionPromptExecutor("") assert.Empty(t, mcls.sentMessage) } func TestExitChecker(t *testing.T) { cases := map[string]struct { in string breakline bool exp bool }{ "Exit with q": { "q", true, true, }, "Exit with end": { "end", true, true, }, "Exit with exit": { "exit", true, true, }, "Don't exit with q": { "q", false, false, }, "Don't exit with end": { "end", false, false, }, "Don't exit with exit": { "exit", false, false, }, "Don't exit 1": { "leave", true, false, }, "Don't exit 2": { "leave", false, false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { assert.Equal(t, tc.exp, exitChecker(tc.in, tc.breakline)) }) } } // CommandHistory tests func TestNewCommandHistory(t *testing.T) { filepath := createTempFile(t) actual, err := newCommandHistory(filepath) assert.NoError(t, err) assert.Equal(t, filepath, actual.file.Name()) assert.NotNil(t, actual.history) } func TestReadFromFile(t *testing.T) { d := t.TempDir() f, err := os.CreateTemp(d, "") assert.NoError(t, err) filepath := f.Name() expected := []string{"good", "day", "world"} for _, s := range expected { _, err = f.WriteString(s + "\n") assert.NoError(t, err) } ch, err := newCommandHistory(filepath) assert.NoError(t, err) err = ch.readFromFile() assert.NoError(t, err) assert.Equal(t, expected, ch.history) } func TestUpdateHistory(t *testing.T) { filepath := createTempFile(t) ch, err := newCommandHistory(filepath) assert.NoError(t, err) expected := []string{"good", "day", "world"} for _, s := range expected { err = ch.updateHistory(s, historyFileLimit) assert.NoError(t, err) } data, err := os.ReadFile(filepath) assert.NoError(t, err) actual := strings.Fields(string(data)) assert.Equal(t, expected, actual) } func TestUpdateHistoryLimit(t *testing.T) { filepath := createTempFile(t) ch, err := newCommandHistory(filepath) assert.NoError(t, err) expected := []string{"day", "world"} in := append([]string{"good"}, expected...) for _, s := range in { err = ch.updateHistory(s, 2) assert.NoError(t, err) } assert.Equal(t, expected, ch.history) } func TestSessionTimeout(t *testing.T) { // mocking mcls := cliService{} mcls.idleTime = time.Now() ctx := context.Background() os.Setenv("RCLI_SESSION_TIMEOUT", "100ms") config := NewConfig(ctx, t.TempDir()) em := NewEmulator(ctx, &mcls, config) time.Sleep(200 * time.Millisecond) // test go em.handleExitEvents(ctx) assert.True(t, <-timeoutChan) } func TestSessionExitChecker(t *testing.T) { // mocking mcls := cliService{} ctx := context.Background() config := NewConfig(ctx, t.TempDir()) em := NewEmulator(ctx, &mcls, config) cases := map[string]struct { in string breakline bool exp bool }{ "Exit with q": { "q", true, true, }, "Exit with end": { "end", true, true, }, "Exit with exit": { "exit", true, true, }, "Don't exit with q": { "q", false, false, }, "Don't exit with end": { "end", false, false, }, "Don't exit with exit": { "exit", false, false, }, "Don't exit 1": { "leave", true, false, }, "Don't exit 2": { "leave", false, false, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { assert.Equal(t, tc.exp, em.sessionPromptExitHandler(tc.in, tc.breakline)) }) } }