package basic import ( "bufio" "fmt" "net/url" "os" "reflect" "strconv" "strings" "testing" "text/template" "github.com/fatih/color" "github.com/nicholas-fedor/shoutrrr/pkg/format" "github.com/nicholas-fedor/shoutrrr/pkg/types" ) // mockConfig implements types.ServiceConfig. type mockConfig struct { Host string `default:"localhost" key:"host"` Port int `default:"8080" key:"port" required:"true"` url *url.URL } func (m *mockConfig) Enums() map[string]types.EnumFormatter { return nil } func (m *mockConfig) GetURL() *url.URL { if m.url == nil { u, _ := url.Parse("mock://url") m.url = u } return m.url } func (m *mockConfig) SetURL(u *url.URL) error { m.url = u return nil } func (m *mockConfig) SetTemplateFile(_ string, _ string) error { return nil } func (m *mockConfig) SetTemplateString(_ string, _ string) error { return nil } func (m *mockConfig) SetLogger(_ types.StdLogger) { // Minimal implementation, no-op } // ConfigQueryResolver methods. func (m *mockConfig) Get(key string) (string, error) { switch strings.ToLower(key) { case "host": return m.Host, nil case "port": return strconv.Itoa(m.Port), nil default: return "", fmt.Errorf("unknown key: %s", key) } } func (m *mockConfig) Set(key string, value string) error { switch strings.ToLower(key) { case "host": m.Host = value return nil case "port": port, err := strconv.Atoi(value) if err != nil { return err } m.Port = port return nil default: return fmt.Errorf("unknown key: %s", key) } } func (m *mockConfig) QueryFields() []string { return []string{"host", "port"} } // mockServiceConfig is a test implementation of Service. type mockServiceConfig struct { Config *mockConfig } func (m *mockServiceConfig) GetID() string { return "mockID" } func (m *mockServiceConfig) GetTemplate(_ string) (*template.Template, bool) { return nil, false } func (m *mockServiceConfig) SetTemplateFile(_ string, _ string) error { return nil } func (m *mockServiceConfig) SetTemplateString(_ string, _ string) error { return nil } func (m *mockServiceConfig) Initialize(_ *url.URL, _ types.StdLogger) error { return nil } func (m *mockServiceConfig) Send(_ string, _ *types.Params) error { return nil } func (m *mockServiceConfig) SetLogger(_ types.StdLogger) {} // ConfigProp methods. func (m *mockConfig) SetFromProp(propValue string) error { // Minimal implementation for testing; typically parses propValue parts := strings.SplitN(propValue, ":", 2) if len(parts) == 2 { m.Host = parts[0] port, err := strconv.Atoi(parts[1]) if err != nil { return err } m.Port = port } return nil } func (m *mockConfig) GetPropValue() (string, error) { // Minimal implementation for testing return fmt.Sprintf("%s:%d", m.Host, m.Port), nil } // newMockServiceConfig creates a new mockServiceConfig with an initialized Config. func newMockServiceConfig() *mockServiceConfig { return &mockServiceConfig{ Config: &mockConfig{}, } } func TestGenerator_Generate(t *testing.T) { tests := []struct { name string props map[string]string input string want types.ServiceConfig wantErr bool }{ { name: "successful generation with defaults", props: map[string]string{}, input: "\n8080\n", want: &mockConfig{ Host: "localhost", Port: 8080, }, wantErr: false, }, { name: "successful generation with props", props: map[string]string{"host": "example.com", "port": "9090"}, input: "", want: &mockConfig{ Host: "example.com", Port: 9090, }, wantErr: false, }, { name: "error_on_invalid_port", props: map[string]string{}, input: "\ninvalid\n", want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &Generator{} // Set up pipe for stdin r, w, err := os.Pipe() if err != nil { t.Fatal(err) } originalStdin := os.Stdin os.Stdin = r defer func() { os.Stdin = originalStdin w.Close() }() // Write input to the pipe _, err = w.WriteString(tt.input) if err != nil { t.Fatal(err) } w.Close() service := newMockServiceConfig() color.NoColor = true got, err := g.Generate(service, tt.props, nil) if (err != nil) != tt.wantErr { t.Errorf("Generate() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { t.Errorf("Generate() = %+v, want %+v", got, tt.want) } }) } } func TestGenerator_promptUserForFields(t *testing.T) { tests := []struct { name string config reflect.Value props map[string]string input string wantErr bool }{ { name: "valid input with defaults", config: reflect.ValueOf(newMockServiceConfig().Config), // Pass *mockConfig props: map[string]string{}, input: "\n8080\n", wantErr: false, }, { name: "valid props", config: reflect.ValueOf(newMockServiceConfig().Config), // Pass *mockConfig props: map[string]string{"host": "test.com", "port": "1234"}, input: "", wantErr: false, }, { name: "invalid config type", config: reflect.ValueOf("not a config"), props: map[string]string{}, input: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &Generator{} scanner := bufio.NewScanner(strings.NewReader(tt.input)) color.NoColor = true err := g.promptUserForFields(tt.config, tt.props, scanner) if (err != nil) != tt.wantErr { t.Errorf("promptUserForFields() error = %v, wantErr %v", err, tt.wantErr) } if err == nil && tt.config.Kind() == reflect.Ptr && tt.config.Type().Elem().Kind() == reflect.Struct { got := tt.config.Interface().(*mockConfig) if tt.props["host"] != "" && got.Host != tt.props["host"] { t.Errorf("promptUserForFields() host = %v, want %v", got.Host, tt.props["host"]) } if tt.props["port"] != "" { wantPort := atoiOrZero(tt.props["port"]) if got.Port != wantPort { t.Errorf("promptUserForFields() port = %v, want %v", got.Port, wantPort) } } } }) } } func TestGenerator_getInputValue(t *testing.T) { tests := []struct { name string field *format.FieldInfo propKey string props map[string]string input string want string wantErr bool }{ { name: "from props", field: &format.FieldInfo{Name: "Host"}, propKey: "host", props: map[string]string{"host": "example.com"}, input: "", want: "example.com", wantErr: false, }, { name: "from user input", field: &format.FieldInfo{Name: "Port", Type: reflect.TypeOf(0)}, // Add Type propKey: "port", props: map[string]string{}, input: "8080\n", want: "8080", wantErr: false, }, { name: "default value", field: &format.FieldInfo{Name: "Host", DefaultValue: "localhost"}, propKey: "host", props: map[string]string{}, input: "\n", want: "localhost", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &Generator{} scanner := bufio.NewScanner(strings.NewReader(tt.input)) color.NoColor = true got, err := g.getInputValue(tt.field, tt.propKey, tt.props, scanner) if (err != nil) != tt.wantErr { t.Errorf("getInputValue() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("getInputValue() = %v, want %v", got, tt.want) } if tt.props[tt.propKey] != "" { t.Errorf("getInputValue() did not clear prop, got %v", tt.props[tt.propKey]) } }) } } func TestGenerator_formatPrompt(t *testing.T) { tests := []struct { name string field *format.FieldInfo want string }{ { name: "field with default", field: &format.FieldInfo{Name: "Host", DefaultValue: "localhost"}, want: "\x1b[97mHost\x1b[0m[localhost]: ", }, { name: "field without default", field: &format.FieldInfo{Name: "Port"}, want: "\x1b[97mPort\x1b[0m: ", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &Generator{} color.NoColor = false got := g.formatPrompt(tt.field) if got != tt.want { t.Errorf("formatPrompt() = %q, want %q", got, tt.want) } }) } } func TestGenerator_setFieldValue(t *testing.T) { tests := []struct { name string config reflect.Value field *format.FieldInfo inputValue string want bool wantErr bool }{ { name: "valid value", config: reflect.ValueOf(newMockServiceConfig().Config).Elem(), field: &format.FieldInfo{Name: "Port", Type: reflect.TypeOf(0), Required: true}, inputValue: "8080", want: true, wantErr: false, }, { name: "required field empty", config: reflect.ValueOf(newMockServiceConfig().Config).Elem(), field: &format.FieldInfo{Name: "Port", Type: reflect.TypeOf(0), Required: true}, inputValue: "", want: false, wantErr: false, }, { name: "invalid value", config: reflect.ValueOf(newMockServiceConfig().Config).Elem(), field: &format.FieldInfo{Name: "Port", Type: reflect.TypeOf(0)}, inputValue: "invalid", want: false, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &Generator{} color.NoColor = true got, err := g.setFieldValue(tt.config, tt.field, tt.inputValue) if (err != nil) != tt.wantErr { t.Errorf("setFieldValue() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("setFieldValue() = %v, want %v", got, tt.want) } if got && !tt.wantErr { if tt.field.Name == "Port" { wantPort := atoiOrZero(tt.inputValue) if gotPort := tt.config.FieldByName("Port").Int(); int(gotPort) != wantPort { t.Errorf("setFieldValue() set Port = %v, want %v", gotPort, wantPort) } } } }) } } func TestGenerator_printError(t *testing.T) { tests := []struct { name string fieldName string errorMsg string }{ { name: "basic error", fieldName: "Port", errorMsg: "invalid format", }, } for _, tt := range tests { t.Run(tt.name, func(*testing.T) { g := &Generator{} color.NoColor = true g.printError(tt.fieldName, tt.errorMsg) }) } } func TestGenerator_printInvalidType(t *testing.T) { tests := []struct { name string fieldName string typeName string }{ { name: "invalid type", fieldName: "Port", typeName: "int", }, } for _, tt := range tests { t.Run(tt.name, func(*testing.T) { g := &Generator{} color.NoColor = true g.printInvalidType(tt.fieldName, tt.typeName) }) } } func TestGenerator_validateAndReturnConfig(t *testing.T) { tests := []struct { name string config reflect.Value want types.ServiceConfig wantErr bool }{ { name: "valid config", config: reflect.ValueOf(&mockConfig{Host: "test", Port: 1234}), want: &mockConfig{Host: "test", Port: 1234}, wantErr: false, }, { name: "invalid config type", config: reflect.ValueOf("not a config"), want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &Generator{} got, err := g.validateAndReturnConfig(tt.config) if (err != nil) != tt.wantErr { t.Errorf("validateAndReturnConfig() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("validateAndReturnConfig() = %v, want %v", got, tt.want) } }) } } // atoiOrZero converts a string to an int, returning 0 on error. func atoiOrZero(s string) int { i, _ := strconv.Atoi(s) return i }