Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 0 additions & 91 deletions internal/commands/connect_start_wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/basecamp/basecamp-cli/internal/appctx"
"github.com/basecamp/basecamp-cli/internal/auth"
"github.com/basecamp/basecamp-cli/internal/config"
"github.com/basecamp/basecamp-cli/internal/connector"
Expand Down Expand Up @@ -483,52 +482,6 @@ func TestAnOversizedServerFaultIsWaitedOut(t *testing.T) {
assert.Equal(t, 2, mints())
}

// A second connector for an agent that is already running says so at once,
// even while the token endpoint is rate-limiting it: the lock is checked
// before the start waits for a token, not after.
func TestASecondConnectorIsRefusedBeforeItWaitsForAToken(t *testing.T) {
f := newOperatorFixture(t)
dir, err := connectStateDir("agent", f.file, false)
require.NoError(t, err)
lock, err := connector.AcquireInstanceLock(dir, f.file.AccountID, f.file.Agent.PersonID, time.Now())
require.NoError(t, err)
t.Cleanup(func() { _ = lock.Release() })

app := newConnectSetupApp(t, f.s, "agent")
creds, err := app.Auth.GetStore().Load(app.Auth.CredentialKey())
require.NoError(t, err)
creds.ExpiresAt = time.Now().Add(-time.Minute).Unix()
require.NoError(t, app.Auth.GetStore().Save(app.Auth.CredentialKey(), creds))
f.s.mu.Lock()
f.s.rateLimitMints, f.s.mints = true, 0
f.s.mu.Unlock()

ctx, cancel := context.WithCancel(t.Context())
defer cancel()
var buf bytes.Buffer
app.Output = output.New(output.Options{Format: output.FormatJSON, Writer: &buf})
cmd := NewConnectCmd()
cmd.SetArgs(nil)
cmd.SetContext(appctx.WithApp(ctx, app))
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SilenceErrors, cmd.SilenceUsage = true, true
done := make(chan error, 1)
go func() { done <- cmd.Execute() }()

select {
case err := <-done:
assert.Equal(t, output.CodeLockUnavailable, usageError(t, err).Code, buf.String())
case <-time.After(10 * time.Second):
cancel()
<-done
t.Fatalf("a second connector waited for a token instead of refusing: %s", buf.String())
}
f.s.mu.Lock()
defer f.s.mu.Unlock()
assert.Zero(t, f.s.mints, "nothing was minted for a connector that cannot run")
}

// A 503 that names its Retry-After is waited out for that long, by the
// start and the feed alike, not on backoff.
func TestAServerFaultsRetryAfterIsHonored(t *testing.T) {
Expand Down Expand Up @@ -564,50 +517,6 @@ func (b *lockedBuffer) String() string {
return b.buf.String()
}

// A start waiting for its token does not hold the agent's lock: until the
// credential is proven to be that agent, the lock is only looked at, so a
// profile whose credential is some other agent's cannot keep the real
// connector out while it waits.
func TestAStartWaitingForItsTokenHoldsNoLock(t *testing.T) {
f := newOperatorFixture(t)
app := newConnectSetupApp(t, f.s, "agent")
creds, err := app.Auth.GetStore().Load(app.Auth.CredentialKey())
require.NoError(t, err)
creds.ExpiresAt = time.Now().Add(-time.Minute).Unix()
require.NoError(t, app.Auth.GetStore().Save(app.Auth.CredentialKey(), creds))
f.s.mu.Lock()
f.s.rateLimitMints = true
f.s.mu.Unlock()

ctx, cancel := context.WithCancel(t.Context())
defer cancel()
var out lockedBuffer
app.Output = output.New(output.Options{Format: output.FormatJSON, Writer: &out})
cmd := NewConnectCmd()
cmd.SetArgs(nil)
cmd.SetContext(appctx.WithApp(ctx, app))
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SilenceErrors, cmd.SilenceUsage = true, true
done := make(chan error, 1)
go func() { done <- cmd.Execute() }()

require.Eventually(t, func() bool { return strings.Contains(out.String(), "could not get the agent's token yet") },
10*time.Second, 20*time.Millisecond, "the start never waited: %s", out.String())
dir, err := connectStateDir("agent", f.file, false)
require.NoError(t, err)
lock, err := connector.AcquireInstanceLock(dir, f.file.AccountID, f.file.Agent.PersonID, time.Now())
require.NoError(t, err, "a waiting start held the agent's lock")
require.NoError(t, lock.Release())

cancel()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatal("the waiting start did not stop when canceled")
}
}

// okTokens is a renewal that succeeds at once.
type okTokens struct{}

Expand Down
108 changes: 108 additions & 0 deletions internal/commands/connect_start_wait_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//go:build unix

package commands

import (
"bytes"
"context"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/basecamp/basecamp-cli/internal/appctx"
"github.com/basecamp/basecamp-cli/internal/connector"
"github.com/basecamp/basecamp-cli/internal/output"
)

// A second connector for an agent that is already running says so at once,
// even while the token endpoint is rate-limiting it: the lock is checked
// before the start waits for a token, not after.
func TestASecondConnectorIsRefusedBeforeItWaitsForAToken(t *testing.T) {
f := newOperatorFixture(t)
dir, err := connectStateDir("agent", f.file, false)
require.NoError(t, err)
lock, err := connector.AcquireInstanceLock(dir, f.file.AccountID, f.file.Agent.PersonID, time.Now())
require.NoError(t, err)
t.Cleanup(func() { _ = lock.Release() })

app := newConnectSetupApp(t, f.s, "agent")
creds, err := app.Auth.GetStore().Load(app.Auth.CredentialKey())
require.NoError(t, err)
creds.ExpiresAt = time.Now().Add(-time.Minute).Unix()
require.NoError(t, app.Auth.GetStore().Save(app.Auth.CredentialKey(), creds))
f.s.mu.Lock()
f.s.rateLimitMints, f.s.mints = true, 0
f.s.mu.Unlock()

ctx, cancel := context.WithCancel(t.Context())
defer cancel()
var buf bytes.Buffer
app.Output = output.New(output.Options{Format: output.FormatJSON, Writer: &buf})
cmd := NewConnectCmd()
cmd.SetArgs(nil)
cmd.SetContext(appctx.WithApp(ctx, app))
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SilenceErrors, cmd.SilenceUsage = true, true
done := make(chan error, 1)
go func() { done <- cmd.Execute() }()

select {
case err := <-done:
assert.Equal(t, output.CodeLockUnavailable, usageError(t, err).Code, buf.String())
case <-time.After(10 * time.Second):
cancel()
<-done
t.Fatalf("a second connector waited for a token instead of refusing: %s", buf.String())
}
f.s.mu.Lock()
defer f.s.mu.Unlock()
assert.Zero(t, f.s.mints, "nothing was minted for a connector that cannot run")
}

// A start waiting for its token does not hold the agent's lock: until the
// credential is proven to be that agent, the lock is only looked at, so a
// profile whose credential is some other agent's cannot keep the real
// connector out while it waits.
func TestAStartWaitingForItsTokenHoldsNoLock(t *testing.T) {
f := newOperatorFixture(t)
app := newConnectSetupApp(t, f.s, "agent")
creds, err := app.Auth.GetStore().Load(app.Auth.CredentialKey())
require.NoError(t, err)
creds.ExpiresAt = time.Now().Add(-time.Minute).Unix()
require.NoError(t, app.Auth.GetStore().Save(app.Auth.CredentialKey(), creds))
f.s.mu.Lock()
f.s.rateLimitMints = true
f.s.mu.Unlock()

ctx, cancel := context.WithCancel(t.Context())
defer cancel()
var out lockedBuffer
app.Output = output.New(output.Options{Format: output.FormatJSON, Writer: &out})
cmd := NewConnectCmd()
cmd.SetArgs(nil)
cmd.SetContext(appctx.WithApp(ctx, app))
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SilenceErrors, cmd.SilenceUsage = true, true
done := make(chan error, 1)
go func() { done <- cmd.Execute() }()

require.Eventually(t, func() bool { return strings.Contains(out.String(), "could not get the agent's token yet") },
10*time.Second, 20*time.Millisecond, "the start never waited: %s", out.String())
dir, err := connectStateDir("agent", f.file, false)
require.NoError(t, err)
lock, err := connector.AcquireInstanceLock(dir, f.file.AccountID, f.file.Agent.PersonID, time.Now())
require.NoError(t, err, "a waiting start held the agent's lock")
require.NoError(t, lock.Release())

cancel()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatal("the waiting start did not stop when canceled")
}
}
Loading