Skip to content

Epic: Phase 2 - Testing Infrastructure #2

@rianjs

Description

@rianjs

Phase 2: Testing Infrastructure

Priority: P0 - Critical for safe refactoring
Estimated Sub-tasks: 4
Blocked By: Phase 1 (Foundation)

Summary

Add comprehensive test coverage before any major refactoring. This includes unit tests for the API client, credential storage, and command handlers, plus a manual integration test specification.

Current State: 0 test files, 0% coverage across 1,617 lines of code.


Sub-Tasks

1. Add Unit Tests for API Client

  • Create internal/client/client_test.go
  • Add constructor for test injection: NewClientWithBaseURL(baseURL, token string)
  • Test client creation and configuration
  • Test authentication header injection
  • Test successful GET/POST requests
  • Test error handling (4xx, 5xx responses)
  • Test Slack API error parsing ({"ok": false, "error": "..."})
  • Test pagination handling for list endpoints
  • Achieve >80% coverage for client.go

Implementation Pattern:

func TestClient_ListChannels_Success(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Equal(t, "GET", r.Method)
        assert.Contains(t, r.URL.Path, "conversations.list")
        
        auth := r.Header.Get("Authorization")
        assert.True(t, strings.HasPrefix(auth, "Bearer "))
        
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"ok": true, "channels": [{"id": "C123", "name": "general"}]}`))
    }))
    defer server.Close()
    
    client := NewClientWithBaseURL(server.URL, "test-token")
    channels, err := client.ListChannels("", true, 100)
    
    require.NoError(t, err)
    assert.Len(t, channels, 1)
    assert.Equal(t, "C123", channels[0].ID)
}

func TestClient_APIError(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"ok": false, "error": "channel_not_found"}`))
    }))
    defer server.Close()
    
    client := NewClientWithBaseURL(server.URL, "test-token")
    _, err := client.GetChannelInfo("C999")
    
    require.Error(t, err)
    assert.Contains(t, err.Error(), "channel_not_found")
}

func TestClient_Pagination(t *testing.T) {
    callCount := 0
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        callCount++
        if callCount == 1 {
            w.Write([]byte(`{"ok": true, "channels": [{"id": "C1"}], "response_metadata": {"next_cursor": "abc"}}`))
        } else {
            w.Write([]byte(`{"ok": true, "channels": [{"id": "C2"}], "response_metadata": {"next_cursor": ""}}`))
        }
    }))
    defer server.Close()
    
    client := NewClientWithBaseURL(server.URL, "test-token")
    channels, err := client.ListChannels("", true, 100)
    
    require.NoError(t, err)
    assert.Len(t, channels, 2)
    assert.Equal(t, 2, callCount)
}

Test Cases:

Test Case Expected
ListChannels success Returns channel list
ListChannels with pagination Follows cursor, returns all
GetChannelInfo success Returns single channel
GetChannelInfo not found Returns error with message
SendMessage success Returns message with timestamp
Network error Returns wrapped error
Invalid JSON response Returns parse error

2. Add Unit Tests for Keychain/Credential Storage

  • Create internal/keychain/keychain_test.go
  • Test GetAPIToken() when token exists
  • Test GetAPIToken() when token doesn't exist
  • Test GetAPIToken() with SLACK_API_TOKEN env var override
  • Test SetAPIToken() stores token correctly
  • Test DeleteAPIToken() removes token
  • Test IsSecureStorage() returns correct value
  • Test file permissions on Linux (0600)
  • Use t.TempDir() for file-based tests

Implementation Pattern:

func TestGetAPIToken_FromEnvVar(t *testing.T) {
    t.Setenv("SLACK_API_TOKEN", "xoxb-test-token")
    
    token, err := GetAPIToken()
    require.NoError(t, err)
    assert.Equal(t, "xoxb-test-token", token)
}

func TestSetAPIToken_FilePermissions(t *testing.T) {
    if runtime.GOOS == "darwin" {
        t.Skip("File-based storage only on Linux")
    }
    
    tmpDir := t.TempDir()
    oldConfigDir := configDir
    configDir = tmpDir
    defer func() { configDir = oldConfigDir }()
    
    err := SetAPIToken("test-token")
    require.NoError(t, err)
    
    credPath := filepath.Join(tmpDir, "credentials")
    info, _ := os.Stat(credPath)
    assert.Equal(t, os.FileMode(0600), info.Mode().Perm())
}

func TestGetAPIToken_NotConfigured(t *testing.T) {
    t.Setenv("SLACK_API_TOKEN", "")
    // ... setup to ensure no stored token
    
    _, err := GetAPIToken()
    require.Error(t, err)
    assert.Contains(t, err.Error(), "no API token found")
}

3. Add Unit Tests for Command Handlers

  • Refactor commands to accept injectable API client
  • Refactor commands to accept injectable stdin (for confirmations)
  • Create cmd/channels_test.go
  • Create cmd/users_test.go
  • Create cmd/messages_test.go
  • Create cmd/workspace_test.go
  • Create cmd/config_test.go
  • Test happy path for each command
  • Test error handling for each command
  • Test flag parsing and validation
  • Test output formatting (JSON vs text)

Implementation Pattern:

func TestChannelsList_Success(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"ok": true, "channels": [{"id": "C123", "name": "general", "num_members": 10}]}`))
    }))
    defer server.Close()
    
    var stdout, stderr bytes.Buffer
    cmd := NewCmdChannelsList()
    cmd.SetOut(&stdout)
    cmd.SetErr(&stderr)
    
    // Inject test client
    testClient := client.NewClientWithBaseURL(server.URL, "test-token")
    runListWithClient = func(opts *listOptions) error {
        return runList(opts, testClient)
    }
    
    err := cmd.Execute()
    require.NoError(t, err)
    assert.Contains(t, stdout.String(), "general")
}

func TestChannelsList_JSONOutput(t *testing.T) {
    // ... setup server ...
    
    cmd := NewCmdChannelsList()
    cmd.SetArgs([]string{"--json"})
    
    err := cmd.Execute()
    require.NoError(t, err)
    
    var result []client.Channel
    err = json.Unmarshal(stdout.Bytes(), &result)
    require.NoError(t, err)
    assert.Len(t, result, 1)
}

4. Create integration-tests.md Manual Test Suite

  • Create integration-tests.md at project root
  • Document test environment setup
  • Document test data conventions
  • Create test matrix for each command group
  • Include edge cases section
  • Include test execution checklist

Content Structure:

# Integration Tests

Manual tests for verifying real-world behavior against live Slack API.

## Test Environment Setup

### Prerequisites
- Slack workspace with test channel
- Bot token with required scopes (see slack-app-manifest.yaml)
- Permission to create/delete channels

### Test Data Conventions
- Test channels use `[Test]` prefix in name
- Clean up test data after tests

## Command Tests

### channels list
| Test Case | Command | Expected Result |
|-----------|---------|-----------------|
| List all | `slack-cli channels list` | Shows table of channels |
| JSON output | `slack-cli channels list --json` | Valid JSON array |
| Filter public | `slack-cli channels list --types=public_channel` | Only public channels |
| With limit | `slack-cli channels list --limit 5` | Max 5 results |

### messages send
| Test Case | Command | Expected Result |
|-----------|---------|-----------------|
| Send text | `slack-cli messages send #test "Hello"` | Message appears in channel |
| Send in thread | `slack-cli messages send #test "Reply" --thread 1234.5678` | Reply in thread |
| Send with blocks | `slack-cli messages send #test "Text" --blocks '[...]'` | Block Kit message |

[... continue for all commands ...]

## Edge Cases
| Test Case | Expected Result |
|-----------|-----------------|
| Unicode in channel names | Handled correctly |
| Empty results | Graceful "No channels found" message |
| Invalid channel ID | Clear error message |
| Rate limiting | Error message with retry info |

## Test Execution Checklist
- [ ] Build latest: `make build`
- [ ] Configure token: `slack-cli config set-token`
- [ ] Create test channel: `slack-cli channels create [Test]-integration`
- [ ] Run through all command tests
- [ ] Test edge cases
- [ ] Clean up: `slack-cli channels archive [Test]-integration`

Acceptance Criteria

  • make test runs all tests with race detection
  • make test-cover shows >70% overall coverage
  • Client package has >80% coverage
  • All commands have at least happy-path tests
  • integration-tests.md documents all commands

Dependencies

  • Blocked By: Phase 1 (CI must exist to run tests)
  • Blocks: Phase 3 (need tests before major refactoring)

Files to Create

  • internal/client/client_test.go
  • internal/keychain/keychain_test.go
  • cmd/channels_test.go
  • cmd/users_test.go
  • cmd/messages_test.go
  • cmd/workspace_test.go
  • cmd/config_test.go
  • integration-tests.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions