package command import ( "bytes" "context" "errors" "io" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" "github.com/gochat/gochat/channels/shangwutong/internal/store" "github.com/sirupsen/logrus" ) func TestRootCommandExposesOnlyPlannedOperations(t *testing.T) { root := NewRootCommand() want := map[string]bool{"serve": false, "migrate": false, "reconcile": false, "backup": false, "doctor": false} for _, command := range root.Commands() { if _, ok := want[command.Name()]; ok { want[command.Name()] = true } } for name, found := range want { if !found { t.Fatalf("missing command %s", name) } } } func TestBackupRequiresOutput(t *testing.T) { root := NewRootCommand() root.SetArgs([]string{"backup"}) root.SetOut(&bytes.Buffer{}) if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--output is required") { t.Fatalf("error = %v", err) } } func TestMigrateRejectsUnknownAction(t *testing.T) { t.Setenv("SWT_CONNECTOR_DB_PATH", filepath.Join(t.TempDir(), "connector.db")) root := NewRootCommand() root.SetArgs([]string{"migrate", "down"}) if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "unsupported migration action") { t.Fatalf("error = %v", err) } } func TestLocalAdminURLUsesLoopbackForWildcardListen(t *testing.T) { got, err := localAdminURL(":9200") if err != nil || got != "http://127.0.0.1:9200" { t.Fatalf("URL = %q, %v", got, err) } } func TestStartupReconcileRetriesUntilFirstCompleteSnapshot(t *testing.T) { attempts := 0 logger := logrus.New() logger.SetOutput(io.Discard) reconcileUntilSuccessful(context.Background(), func(context.Context) error { attempts++ if attempts == 1 { return errors.New("GoChat unavailable") } return nil }, logrus.NewEntry(logger), time.Millisecond) if attempts != 2 { t.Fatalf("attempts = %d", attempts) } } func TestOperationalCommands(t *testing.T) { directory := t.TempDir() databasePath, backupPath := filepath.Join(directory, "connector.db"), filepath.Join(directory, "backup.db") t.Setenv("SWT_CONNECTOR_DB_PATH", databasePath) for _, args := range [][]string{{"migrate", "up"}, {"migrate", "status"}, {"backup", "--output", backupPath}} { root, output := NewRootCommand(), &bytes.Buffer{} root.SetArgs(args) root.SetOut(output) if err := root.Execute(); err != nil { t.Fatalf("%v: %v", args, err) } if output.Len() == 0 { t.Fatalf("%v produced no output", args) } } if version, err := store.InspectDatabase(context.Background(), backupPath); err != nil || version != 2 { t.Fatalf("backup version = %d, %v", version, err) } goChat := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.URL.Path != "/api/v1/connector/shangwutong/inboxes" || request.Header.Get("Authorization") != "Bearer service-token" { http.Error(response, "unexpected request", http.StatusBadRequest) return } response.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(response, `{"data":[],"next_cursor":""}`) })) defer goChat.Close() t.Setenv("GOCHAT_BASE_URL", goChat.URL) t.Setenv("GOCHAT_CONNECTOR_SERVICE_TOKEN", "service-token") root, output := NewRootCommand(), &bytes.Buffer{} root.SetArgs([]string{"doctor"}) root.SetOut(output) if err := root.Execute(); err != nil || !strings.Contains(output.String(), "inboxes=0") { t.Fatalf("doctor output=%q err=%v", output.String(), err) } reconcile := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost || request.URL.Path != "/internal/reconcile" { http.Error(response, "unexpected request", http.StatusBadRequest) return } response.WriteHeader(http.StatusAccepted) })) defer reconcile.Close() t.Setenv("SWT_CONNECTOR_LISTEN", strings.TrimPrefix(reconcile.URL, "http://")) root, output = NewRootCommand(), &bytes.Buffer{} root.SetArgs([]string{"reconcile"}) root.SetOut(output) if err := root.Execute(); err != nil || !strings.Contains(output.String(), "reconcile accepted") { t.Fatalf("reconcile output=%q err=%v", output.String(), err) } }