package main import ( "context" "strings" "testing" "github.com/nomadcode/nomadcode-core/internal/adapters/plane" ) type mockPlaneClient struct { getItemFunc func(ctx context.Context, ref plane.WorkItemRef) (plane.WorkItem, error) addCommentFunc func(ctx context.Context, input plane.AddCommentInput) error updateFunc func(ctx context.Context, input plane.UpdateIssueStatusInput) error } func (m *mockPlaneClient) GetWorkItem(ctx context.Context, ref plane.WorkItemRef) (plane.WorkItem, error) { return m.getItemFunc(ctx, ref) } func (m *mockPlaneClient) AddComment(ctx context.Context, input plane.AddCommentInput) error { return m.addCommentFunc(ctx, input) } func (m *mockPlaneClient) UpdateIssueStatus(ctx context.Context, input plane.UpdateIssueStatusInput) error { return m.updateFunc(ctx, input) } func TestLoadConfig(t *testing.T) { tests := []struct { name string env map[string]string wantErr bool errMsg string }{ { name: "valid read-only config", env: map[string]string{ "PLANE_BASE_URL": "https://plane.example.com", "PLANE_TOKEN": "secret-token", "PLANE_WORKSPACE_SLUG": "general", "PLANE_PROJECT_ID": "proj-1", "PLANE_WORK_ITEM_ID": "item-1", }, wantErr: false, }, { name: "missing required fields", env: map[string]string{ "PLANE_BASE_URL": "https://plane.example.com", }, wantErr: true, errMsg: "missing required environment variables", }, { name: "valid write config", env: map[string]string{ "PLANE_BASE_URL": "https://plane.example.com", "PLANE_TOKEN": "secret-token", "PLANE_WORKSPACE_SLUG": "general", "PLANE_PROJECT_ID": "proj-1", "PLANE_WORK_ITEM_ID": "item-1", "PLANE_SMOKE_APPLY": "1", "PLANE_SMOKE_COMMENT": "test comment", "PLANE_SMOKE_STATE_ID": "state-done", }, wantErr: false, }, { name: "missing write fields when apply=1", env: map[string]string{ "PLANE_BASE_URL": "https://plane.example.com", "PLANE_TOKEN": "secret-token", "PLANE_WORKSPACE_SLUG": "general", "PLANE_PROJECT_ID": "proj-1", "PLANE_WORK_ITEM_ID": "item-1", "PLANE_SMOKE_APPLY": "1", }, wantErr: true, errMsg: "missing required smoke variables", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { getenv := func(key string) string { return tt.env[key] } cfg, err := LoadConfig(getenv) if (err != nil) != tt.wantErr { t.Fatalf("LoadConfig() error = %v, wantErr = %v", err, tt.wantErr) } if err != nil && tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { t.Fatalf("expected error containing %q, got %q", tt.errMsg, err.Error()) } if err == nil { if cfg.BaseURL != tt.env["PLANE_BASE_URL"] { t.Errorf("expected BaseURL %q, got %q", tt.env["PLANE_BASE_URL"], cfg.BaseURL) } if cfg.Token != tt.env["PLANE_TOKEN"] { t.Errorf("expected Token %q, got %q", tt.env["PLANE_TOKEN"], cfg.Token) } } }) } } func TestRunSmoke(t *testing.T) { cfg := EnvConfig{ BaseURL: "https://plane.example.com", Token: "secret-token", WorkspaceSlug: "general", ProjectID: "proj-1", WorkItemID: "item-1", } t.Run("read-only mode", func(t *testing.T) { var builder strings.Builder client := &mockPlaneClient{ getItemFunc: func(ctx context.Context, ref plane.WorkItemRef) (plane.WorkItem, error) { return plane.WorkItem{ID: "item-1", Name: "Test Issue"}, nil }, } err := RunSmoke(context.Background(), cfg, client, &builder) if err != nil { t.Fatalf("unexpected error: %v", err) } out := builder.String() if !strings.Contains(out, "lookup ok: id=item-1 name=Test Issue") { t.Errorf("unexpected output: %q", out) } if !strings.Contains(out, "write smoke skipped") { t.Errorf("expected write smoke skipped message, got %q", out) } }) t.Run("write apply mode", func(t *testing.T) { writeCfg := cfg writeCfg.SmokeApply = true writeCfg.SmokeComment = "test comment" writeCfg.SmokeStateID = "state-done" var builder strings.Builder var commentCalled, stateCalled bool client := &mockPlaneClient{ getItemFunc: func(ctx context.Context, ref plane.WorkItemRef) (plane.WorkItem, error) { return plane.WorkItem{ID: "item-1", Name: "Test Issue"}, nil }, addCommentFunc: func(ctx context.Context, input plane.AddCommentInput) error { commentCalled = true if input.CommentHTML != "
test comment
" { t.Errorf("unexpected comment body: %q", input.CommentHTML) } return nil }, updateFunc: func(ctx context.Context, input plane.UpdateIssueStatusInput) error { stateCalled = true if input.Status != "state-done" { t.Errorf("unexpected status: %q", input.Status) } return nil }, } err := RunSmoke(context.Background(), writeCfg, client, &builder) if err != nil { t.Fatalf("unexpected error: %v", err) } if !commentCalled || !stateCalled { t.Errorf("expected write methods to be called: comment=%t, state=%t", commentCalled, stateCalled) } out := builder.String() if !strings.Contains(out, "lookup ok") || !strings.Contains(out, "comment ok") || !strings.Contains(out, "state update ok") { t.Errorf("unexpected output: %q", out) } }) } func TestRedactToken(t *testing.T) { token := "super-secret-key-123" errStr := "api error with token super-secret-key-123 failed" redacted := redactToken(errStr, token) if strings.Contains(redacted, token) { t.Errorf("token was not redacted: %q", redacted) } if !strings.Contains(redacted, "