Skip to content

Commit 2332baf

Browse files
committed
refactor: make bootstrap state the onboarding source of truth
1 parent 6237fe6 commit 2332baf

6 files changed

Lines changed: 45 additions & 73 deletions

File tree

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,20 @@
11
package onboarding
22

33
import (
4-
"github.com/usetero/cli/internal/core/bootstrap"
54
"github.com/usetero/cli/internal/domain"
65
)
76

8-
func (m *Model) toCoreState() bootstrap.State {
9-
return bootstrap.State{
10-
User: m.state.user,
11-
Org: m.state.org,
12-
Account: m.state.account,
13-
Workspace: m.state.workspace,
14-
DDSite: m.state.ddSite,
15-
DDAPIKey: m.state.ddAPIKey,
16-
DDAccount: m.state.ddAccount,
17-
}
18-
}
7+
func (m *Model) toCoreState() onboardingState { return m.state }
198

20-
func (m *Model) applyCoreState(state bootstrap.State) {
21-
m.state.user = state.User
22-
m.state.org = state.Org
23-
m.state.account = state.Account
24-
m.state.workspace = state.Workspace
25-
m.state.ddSite = state.DDSite
26-
m.state.ddAPIKey = state.DDAPIKey
27-
m.state.ddAccount = state.DDAccount
9+
func (m *Model) applyCoreState(state onboardingState) {
10+
m.state = state
2811
m.syncServicesToState()
2912
}
3013

3114
func (m *Model) syncServicesToState() {
3215
accountID := domain.AccountID("")
33-
if m.state.account != nil {
34-
accountID = m.state.account.ID
16+
if m.state.Account != nil {
17+
accountID = m.state.Account.ID
3518
}
3619
m.services = m.services.WithAccountID(accountID)
3720
}

‎internal/app/onboarding/gate_requirements_test.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ func TestRewindGateFor(t *testing.T) {
1313
}{
1414
{name: "no requirements gate unchanged", target: GateOrgSelect, state: onboardingState{}, want: GateOrgSelect},
1515
{name: "account gate rewinds to org", target: GateAccountSelect, state: onboardingState{}, want: GateOrgSelect},
16-
{name: "datadog check rewinds to account", target: GateDatadogCheck, state: onboardingState{org: ptrOrg("org-1")}, want: GateAccountSelect},
17-
{name: "datadog api rewinds to region when site missing", target: GateDatadogAPIKey, state: onboardingState{org: ptrOrg("org-1"), account: ptrAccount("acc-1")}, want: GateDatadogRegion},
18-
{name: "datadog app rewinds to api key when api key missing", target: GateDatadogAppKey, state: onboardingState{org: ptrOrg("org-1"), account: ptrAccount("acc-1"), ddSite: "US1"}, want: GateDatadogAPIKey},
19-
{name: "discovery rewinds to datadog check without dd account", target: GateDatadogDiscovery, state: onboardingState{org: ptrOrg("org-1"), account: ptrAccount("acc-1")}, want: GateDatadogCheck},
20-
{name: "sync rewinds to workspace", target: GateSync, state: onboardingState{org: ptrOrg("org-1"), account: ptrAccount("acc-1")}, want: GateWorkspaceSelect},
21-
{name: "sync stays when requirements met", target: GateSync, state: onboardingState{org: ptrOrg("org-1"), account: ptrAccount("acc-1"), workspace: ptrWorkspace("ws-1")}, want: GateSync},
16+
{name: "datadog check rewinds to account", target: GateDatadogCheck, state: onboardingState{Org: ptrOrg("org-1")}, want: GateAccountSelect},
17+
{name: "datadog api rewinds to region when site missing", target: GateDatadogAPIKey, state: onboardingState{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1")}, want: GateDatadogRegion},
18+
{name: "datadog app rewinds to api key when api key missing", target: GateDatadogAppKey, state: onboardingState{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1"), DDSite: "US1"}, want: GateDatadogAPIKey},
19+
{name: "discovery rewinds to datadog check without dd account", target: GateDatadogDiscovery, state: onboardingState{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1")}, want: GateDatadogCheck},
20+
{name: "sync rewinds to workspace", target: GateSync, state: onboardingState{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1")}, want: GateWorkspaceSelect},
21+
{name: "sync stays when requirements met", target: GateSync, state: onboardingState{Org: ptrOrg("org-1"), Account: ptrAccount("acc-1"), Workspace: ptrWorkspace("ws-1")}, want: GateSync},
2222
}
2323

2424
for _, tc := range tests {

‎internal/app/onboarding/gate_runner_registry.go‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,34 +68,34 @@ func defaultGateDefinitions() map[Gate]gateDefinition {
6868
return organizations.NewCreate(m.ctx, m.theme, m.services, m.userPrefs, m.scope)
6969
}),
7070
GateAccountSelect: gateDef(GateAccountSelect, func(m *Model) Step {
71-
return accounts.NewSelect(m.ctx, m.theme, *m.state.org, m.services, m.orgPrefs, m.scope)
71+
return accounts.NewSelect(m.ctx, m.theme, *m.state.Org, m.services, m.orgPrefs, m.scope)
7272
}, withRequirement(gateRequirement{NeedsOrg: true})),
7373
GateAccountCreate: gateDef(GateAccountCreate, func(m *Model) Step {
74-
return accounts.NewCreate(m.ctx, m.theme, *m.state.org, m.services, m.orgPrefs, m.scope)
74+
return accounts.NewCreate(m.ctx, m.theme, *m.state.Org, m.services, m.orgPrefs, m.scope)
7575
}, withRequirement(gateRequirement{NeedsOrg: true})),
7676
GateRuntimeInit: gateDef(GateRuntimeInit, func(m *Model) Step {
77-
return runtimeinit.New(m.theme, *m.state.org, *m.state.account, m.scope)
77+
return runtimeinit.New(m.theme, *m.state.Org, *m.state.Account, m.scope)
7878
},
7979
withRequirement(gateRequirement{NeedsOrg: true, NeedsAccount: true}),
8080
withDisplay(gateDisplayPolicy{hidden: true, status: "Initializing account runtime..."}),
8181
),
8282
GateDatadogCheck: gateDef(GateDatadogCheck, func(m *Model) Step {
83-
return datadog.NewCheck(m.ctx, m.theme, *m.state.account, m.services, m.scope)
83+
return datadog.NewCheck(m.ctx, m.theme, *m.state.Account, m.services, m.scope)
8484
}, withRequirement(gateRequirement{NeedsOrg: true, NeedsAccount: true})),
8585
GateDatadogRegion: gateDef(GateDatadogRegion, func(m *Model) Step {
8686
return datadog.NewRegion(m.theme, m.scope)
8787
}),
8888
GateDatadogAPIKey: gateDef(GateDatadogAPIKey, func(m *Model) Step {
89-
return datadog.NewAPIKey(m.ctx, m.theme, *m.state.account, m.state.ddSite, m.services, m.scope)
89+
return datadog.NewAPIKey(m.ctx, m.theme, *m.state.Account, m.state.DDSite, m.services, m.scope)
9090
}, withRequirement(gateRequirement{NeedsOrg: true, NeedsAccount: true, NeedsDDSite: true})),
9191
GateDatadogAppKey: gateDef(GateDatadogAppKey, func(m *Model) Step {
92-
return datadog.NewAppKey(m.ctx, m.theme, *m.state.account, m.state.ddSite, m.state.ddAPIKey, m.services, m.scope)
92+
return datadog.NewAppKey(m.ctx, m.theme, *m.state.Account, m.state.DDSite, m.state.DDAPIKey, m.services, m.scope)
9393
}, withRequirement(gateRequirement{NeedsOrg: true, NeedsAccount: true, NeedsDDSite: true, NeedsDDAPIKey: true})),
9494
GateDatadogDiscovery: gateDef(GateDatadogDiscovery, func(m *Model) Step {
95-
return datadog.NewDiscovery(m.ctx, m.theme, m.state.ddAccount, m.services, m.scope)
95+
return datadog.NewDiscovery(m.ctx, m.theme, m.state.DDAccount, m.services, m.scope)
9696
}, withRequirement(gateRequirement{NeedsOrg: true, NeedsAccount: true, NeedsDDAccount: true})),
9797
GateWorkspaceSelect: gateDef(GateWorkspaceSelect, func(m *Model) Step {
98-
return workspaces.NewSelect(m.ctx, m.theme, *m.state.account, m.services, m.orgPrefs, m.scope)
98+
return workspaces.NewSelect(m.ctx, m.theme, *m.state.Account, m.services, m.orgPrefs, m.scope)
9999
}, withRequirement(gateRequirement{NeedsOrg: true, NeedsAccount: true})),
100100
GateSync: gateDef(GateSync, func(m *Model) Step {
101101
return sync.New(m.theme, m.syncer, m.scope)

‎internal/app/onboarding/state.go‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
11
package onboarding
22

3-
import (
4-
iauth "github.com/usetero/cli/internal/auth"
5-
"github.com/usetero/cli/internal/domain"
6-
)
3+
import "github.com/usetero/cli/internal/core/bootstrap"
74

8-
type onboardingState struct {
9-
user *iauth.User
10-
org *domain.Organization
11-
account *domain.Account
12-
workspace *domain.Workspace
13-
ddSite domain.DatadogSite
14-
ddAPIKey string
15-
ddAccount domain.DatadogAccountID
16-
}
5+
type onboardingState = bootstrap.State

‎internal/app/onboarding/transition_handlers.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ func (m *Model) handlePreflightResolved(msg msgs.PreflightResolved) TransitionOu
2323

2424
nextState, next := bootstrap.ApplyPreflight(
2525
bootstrap.State{
26-
Org: m.state.org,
27-
Account: m.state.account,
26+
Org: m.state.Org,
27+
Account: m.state.Account,
2828
},
2929
bootstrap.PreflightResolved{
3030
Outcome: bootstrap.PreflightOutcome(msg.State.Outcome),
@@ -158,10 +158,10 @@ func (m *Model) handleSyncComplete() TransitionOutcome {
158158
completion, ok := bootstrap.CompleteOnboarding(m.toCoreState())
159159
if !ok {
160160
m.scope.Error("sync completed without required onboarding state",
161-
slog.Bool("has_user", m.state.user != nil),
162-
slog.Bool("has_org", m.state.org != nil),
163-
slog.Bool("has_account", m.state.account != nil),
164-
slog.Bool("has_workspace", m.state.workspace != nil),
161+
slog.Bool("has_user", m.state.User != nil),
162+
slog.Bool("has_org", m.state.Org != nil),
163+
slog.Bool("has_account", m.state.Account != nil),
164+
slog.Bool("has_workspace", m.state.Workspace != nil),
165165
)
166166
return noop()
167167
}

‎internal/app/onboarding/transitions_test.go‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -136,36 +136,36 @@ func TestHandleTransitionWorkspaceSelectedSetsState(t *testing.T) {
136136
if m.gate != GateSync {
137137
t.Fatalf("gate = %s, want %s", m.gate, GateSync)
138138
}
139-
if m.state.workspace == nil || m.state.workspace.ID != workspace.ID {
140-
t.Fatalf("workspace state not set correctly: %+v", m.state.workspace)
139+
if m.state.Workspace == nil || m.state.Workspace.ID != workspace.ID {
140+
t.Fatalf("workspace state not set correctly: %+v", m.state.Workspace)
141141
}
142142
}
143143

144144
func TestHandleTransitionDatadogState(t *testing.T) {
145145
t.Parallel()
146146

147147
m := newTestModel(t)
148-
m.state.account = ptrAccount("acc-1")
148+
m.state.Account = ptrAccount("acc-1")
149149

150150
if _ = m.handleTransition(msgs.DatadogRegionSelected{Site: "US1"}); m.gate != GateDatadogAPIKey {
151151
t.Fatalf("expected datadog region to route to api key gate")
152152
}
153-
if m.state.ddSite != "US1" {
154-
t.Fatalf("ddSite = %s, want US1", m.state.ddSite)
153+
if m.state.DDSite != "US1" {
154+
t.Fatalf("ddSite = %s, want US1", m.state.DDSite)
155155
}
156156

157157
if _ = m.handleTransition(msgs.DatadogAPIKeyEntered{APIKey: "api-key"}); m.gate != GateDatadogAppKey {
158158
t.Fatalf("expected api key entered to route to app key gate")
159159
}
160-
if m.state.ddAPIKey != "api-key" {
161-
t.Fatalf("ddAPIKey = %q, want %q", m.state.ddAPIKey, "api-key")
160+
if m.state.DDAPIKey != "api-key" {
161+
t.Fatalf("ddAPIKey = %q, want %q", m.state.DDAPIKey, "api-key")
162162
}
163163

164164
if _ = m.handleTransition(msgs.DatadogAccountCreated{DatadogAccountID: "dd-1"}); m.gate != GateDatadogDiscovery {
165165
t.Fatalf("expected account created to route to discovery gate")
166166
}
167-
if m.state.ddAccount != "dd-1" {
168-
t.Fatalf("ddAccount = %q, want %q", m.state.ddAccount, "dd-1")
167+
if m.state.DDAccount != "dd-1" {
168+
t.Fatalf("ddAccount = %q, want %q", m.state.DDAccount, "dd-1")
169169
}
170170
}
171171

@@ -179,10 +179,10 @@ func TestHandleTransitionRuntimeReady(t *testing.T) {
179179
if _ = m.handleTransition(msgs.RuntimeReady{Org: *org, Account: *account}); m.gate != GateDatadogCheck {
180180
t.Fatalf("expected runtime ready to route to datadog check")
181181
}
182-
if m.state.org == nil || m.state.org.ID != org.ID {
182+
if m.state.Org == nil || m.state.Org.ID != org.ID {
183183
t.Fatalf("org state not set from runtime ready")
184184
}
185-
if m.state.account == nil || m.state.account.ID != account.ID {
185+
if m.state.Account == nil || m.state.Account.ID != account.ID {
186186
t.Fatalf("account state not set from runtime ready")
187187
}
188188
}
@@ -214,7 +214,7 @@ func TestHandleTransitionOrgSelectedClearsServiceAccountScope(t *testing.T) {
214214
scopedAccountID = accountID
215215
}
216216

217-
m.state.account = ptrAccount("acc-1")
217+
m.state.Account = ptrAccount("acc-1")
218218
_ = m.handleTransition(msgs.OrgSelected{Org: *ptrOrg("org-2")})
219219
if scopedAccountID != "" {
220220
t.Fatalf("scoped account id = %q, want empty", scopedAccountID)
@@ -225,10 +225,10 @@ func TestHandleTransitionSyncComplete(t *testing.T) {
225225
t.Parallel()
226226

227227
m := newTestModel(t)
228-
m.state.user = ptrUser("user-1")
229-
m.state.org = ptrOrg("org-1")
230-
m.state.account = ptrAccount("acc-1")
231-
m.state.workspace = ptrWorkspace("ws-1")
228+
m.state.User = ptrUser("user-1")
229+
m.state.Org = ptrOrg("org-1")
230+
m.state.Account = ptrAccount("acc-1")
231+
m.state.Workspace = ptrWorkspace("ws-1")
232232

233233
cmd := m.handleTransition(msgs.SyncComplete{})
234234
if cmd == nil {
@@ -248,8 +248,8 @@ func TestHandleTransitionSyncCompleteMissingStateNoops(t *testing.T) {
248248
t.Parallel()
249249

250250
m := newTestModel(t)
251-
m.state.user = ptrUser("user-1")
252-
m.state.org = ptrOrg("org-1")
251+
m.state.User = ptrUser("user-1")
252+
m.state.Org = ptrOrg("org-1")
253253
// Missing account/workspace should not panic or emit completion payload.
254254

255255
cmd := m.handleTransition(msgs.SyncComplete{})

0 commit comments

Comments
 (0)