Skip to content

Commit 10ccce1

Browse files
committed
feat: expose 30d trial form via CLI
1 parent 79d24d2 commit 10ccce1

File tree

5 files changed

+359
-1
lines changed

5 files changed

+359
-1
lines changed

cli/login.go

+176
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ func (r *RootCmd) login() *serpent.Command {
149149
password string
150150
trial bool
151151
useTokenForSession bool
152+
153+
firstName string
154+
lastName string
155+
phoneNumber string
156+
jobTitle string
157+
companyName string
158+
country string
159+
developers string
152160
)
153161
cmd := &serpent.Command{
154162
Use: "login [<url>]",
@@ -267,12 +275,66 @@ func (r *RootCmd) login() *serpent.Command {
267275
trial = v == "yes" || v == "y"
268276
}
269277

278+
if trial {
279+
if firstName == "" {
280+
firstName, err = promptTrialInfo(inv, "firstName")
281+
if err != nil {
282+
return err
283+
}
284+
}
285+
if lastName == "" {
286+
lastName, err = promptTrialInfo(inv, "lastName")
287+
if err != nil {
288+
return err
289+
}
290+
}
291+
if phoneNumber == "" {
292+
phoneNumber, err = promptTrialInfo(inv, "phoneNumber")
293+
if err != nil {
294+
return err
295+
}
296+
}
297+
if jobTitle == "" {
298+
jobTitle, err = promptTrialInfo(inv, "jobTitle")
299+
if err != nil {
300+
return err
301+
}
302+
}
303+
if companyName == "" {
304+
companyName, err = promptTrialInfo(inv, "companyName")
305+
if err != nil {
306+
return err
307+
}
308+
}
309+
if country == "" {
310+
country, err = promptCountry(inv)
311+
if err != nil {
312+
return err
313+
}
314+
}
315+
if developers == "" {
316+
developers, err = promptDevelopers(inv)
317+
if err != nil {
318+
return err
319+
}
320+
}
321+
}
322+
270323
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
271324
Email: email,
272325
Username: username,
273326
Name: name,
274327
Password: password,
275328
Trial: trial,
329+
TrialInfo: codersdk.CreateFirstUserTrialInfo{
330+
FirstName: firstName,
331+
LastName: lastName,
332+
PhoneNumber: phoneNumber,
333+
JobTitle: jobTitle,
334+
CompanyName: companyName,
335+
Country: country,
336+
Developers: developers,
337+
},
276338
})
277339
if err != nil {
278340
return xerrors.Errorf("create initial user: %w", err)
@@ -398,6 +460,48 @@ func (r *RootCmd) login() *serpent.Command {
398460
Description: "By default, the CLI will generate a new session token when logging in. This flag will instead use the provided token as the session token.",
399461
Value: serpent.BoolOf(&useTokenForSession),
400462
},
463+
{
464+
Flag: "first-user-first-name",
465+
Env: "CODER_FIRST_USER_FIRST_NAME",
466+
Description: "Specifies the first name of the user.",
467+
Value: serpent.StringOf(&firstName),
468+
},
469+
{
470+
Flag: "first-user-last-name",
471+
Env: "CODER_FIRST_USER_LAST_NAME",
472+
Description: "Specifies the last name of the user.",
473+
Value: serpent.StringOf(&lastName),
474+
},
475+
{
476+
Flag: "first-user-phone-number",
477+
Env: "CODER_FIRST_USER_PHONE_NUMBER",
478+
Description: "Specifies the phone number of the user.",
479+
Value: serpent.StringOf(&phoneNumber),
480+
},
481+
{
482+
Flag: "first-user-job-title",
483+
Env: "CODER_FIRST_USER_JOB_TITLE",
484+
Description: "Specifies the job title of the user.",
485+
Value: serpent.StringOf(&jobTitle),
486+
},
487+
{
488+
Flag: "first-user-company-name",
489+
Env: "CODER_FIRST_USER_COMPANY_NAME",
490+
Description: "Specifies the company name of the user.",
491+
Value: serpent.StringOf(&companyName),
492+
},
493+
{
494+
Flag: "first-user-country",
495+
Env: "CODER_FIRST_USER_COUNTRY",
496+
Description: "Specifies the country of the user.",
497+
Value: serpent.StringOf(&country),
498+
},
499+
{
500+
Flag: "first-user-developers",
501+
Env: "CODER_FIRST_USER_DEVELOPERS",
502+
Description: "Specifies the number of developers.",
503+
Value: serpent.StringOf(&developers),
504+
},
401505
}
402506
return cmd
403507
}
@@ -449,3 +553,75 @@ func openURL(inv *serpent.Invocation, urlToOpen string) error {
449553

450554
return browser.OpenURL(urlToOpen)
451555
}
556+
557+
func promptTrialInfo(inv *serpent.Invocation, fieldName string) (string, error) {
558+
value, err := cliui.Prompt(inv, cliui.PromptOptions{
559+
Text: fmt.Sprintf("Please enter %s:", pretty.Sprint(cliui.DefaultStyles.Field, fieldName)),
560+
Validate: func(s string) error {
561+
if strings.TrimSpace(s) == "" {
562+
return xerrors.Errorf("%s is required", fieldName)
563+
}
564+
return nil
565+
},
566+
})
567+
if err != nil {
568+
if errors.Is(err, cliui.Canceled) {
569+
return "", nil
570+
}
571+
return "", err
572+
}
573+
return value, nil
574+
}
575+
576+
func promptDevelopers(inv *serpent.Invocation) (string, error) {
577+
options := []string{"1-100", "101-500", "501-1000", "1001-2500", "2500+"}
578+
selection, err := cliui.Select(inv, cliui.SelectOptions{
579+
Options: options,
580+
HideSearch: false,
581+
Message: "Select the number of developers:",
582+
})
583+
if err != nil {
584+
return "", xerrors.Errorf("select developers: %w", err)
585+
}
586+
return selection, nil
587+
}
588+
589+
func promptCountry(inv *serpent.Invocation) (string, error) {
590+
countries := []string{
591+
"Afghanistan", "Åland Islands", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda",
592+
"Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados",
593+
"Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia, Plurinational State of", "Bonaire, Sint Eustatius and Saba", "Bosnia and Herzegovina", "Botswana",
594+
"Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", "Cameroon", "Canada",
595+
"Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros",
596+
"Congo", "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Côte d'Ivoire", "Croatia", "Cuba", "Curaçao", "Cyprus", "Czech Republic",
597+
"Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia",
598+
"Ethiopia", "Falkland Islands (Malvinas)", "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon",
599+
"Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam",
600+
"Guatemala", "Guernsey", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard Island and McDonald Islands", "Holy See (Vatican City State)", "Honduras", "Hong Kong",
601+
"Hungary", "Iceland", "India", "Indonesia", "Iran, Islamic Republic of", "Iraq", "Ireland", "Isle of Man", "Israel", "Italy",
602+
"Jamaica", "Japan", "Jersey", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait",
603+
"Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
604+
"Macao", "Macedonia, the Former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique",
605+
"Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montenegro", "Montserrat",
606+
"Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Caledonia", "New Zealand", "Nicaragua",
607+
"Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Palestine, State of",
608+
"Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar",
609+
"Réunion", "Romania", "Russian Federation", "Rwanda", "Saint Barthélemy", "Saint Helena, Ascension and Tristan da Cunha", "Saint Kitts and Nevis", "Saint Lucia", "Saint Martin (French part)", "Saint Pierre and Miquelon",
610+
"Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore",
611+
"Sint Maarten (Dutch part)", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Sudan", "Spain", "Sri Lanka",
612+
"Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of",
613+
"Thailand", "Timor-Leste", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands",
614+
"Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu",
615+
"Venezuela, Bolivarian Republic of", "Viet Nam", "Virgin Islands, British", "Virgin Islands, U.S.", "Wallis and Futuna", "Western Sahara", "Yemen", "Zambia", "Zimbabwe",
616+
}
617+
618+
selection, err := cliui.Select(inv, cliui.SelectOptions{
619+
Options: countries,
620+
Message: "Select the country:",
621+
HideSearch: false,
622+
})
623+
if err != nil {
624+
return "", xerrors.Errorf("select country: %w", err)
625+
}
626+
return selection, nil
627+
}

cli/login_test.go

+89-1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,58 @@ func TestLogin(t *testing.T) {
9696
"password", coderdtest.FirstUserParams.Password,
9797
"password", coderdtest.FirstUserParams.Password, // confirm
9898
"trial", "yes",
99+
"firstName", coderdtest.TrialUserParams.FirstName,
100+
"lastName", coderdtest.TrialUserParams.LastName,
101+
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
102+
"jobTitle", coderdtest.TrialUserParams.JobTitle,
103+
"companyName", coderdtest.TrialUserParams.CompanyName,
104+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
105+
}
106+
for i := 0; i < len(matches); i += 2 {
107+
match := matches[i]
108+
value := matches[i+1]
109+
pty.ExpectMatch(match)
110+
pty.WriteLine(value)
111+
}
112+
pty.ExpectMatch("Welcome to Coder")
113+
<-doneChan
114+
ctx := testutil.Context(t, testutil.WaitShort)
115+
resp, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
116+
Email: coderdtest.FirstUserParams.Email,
117+
Password: coderdtest.FirstUserParams.Password,
118+
})
119+
require.NoError(t, err)
120+
client.SetSessionToken(resp.SessionToken)
121+
me, err := client.User(ctx, codersdk.Me)
122+
require.NoError(t, err)
123+
assert.Equal(t, coderdtest.FirstUserParams.Username, me.Username)
124+
assert.Equal(t, coderdtest.FirstUserParams.Name, me.Name)
125+
assert.Equal(t, coderdtest.FirstUserParams.Email, me.Email)
126+
})
127+
128+
t.Run("InitialUserTTYWithNoTrial", func(t *testing.T) {
129+
t.Parallel()
130+
client := coderdtest.New(t, nil)
131+
// The --force-tty flag is required on Windows, because the `isatty` library does not
132+
// accurately detect Windows ptys when they are not attached to a process:
133+
// https://github.com/mattn/go-isatty/issues/59
134+
doneChan := make(chan struct{})
135+
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
136+
pty := ptytest.New(t).Attach(root)
137+
go func() {
138+
defer close(doneChan)
139+
err := root.Run()
140+
assert.NoError(t, err)
141+
}()
142+
143+
matches := []string{
144+
"first user?", "yes",
145+
"username", coderdtest.FirstUserParams.Username,
146+
"name", coderdtest.FirstUserParams.Name,
147+
"email", coderdtest.FirstUserParams.Email,
148+
"password", coderdtest.FirstUserParams.Password,
149+
"password", coderdtest.FirstUserParams.Password, // confirm
150+
"trial", "no",
99151
}
100152
for i := 0; i < len(matches); i += 2 {
101153
match := matches[i]
@@ -142,6 +194,12 @@ func TestLogin(t *testing.T) {
142194
"password", coderdtest.FirstUserParams.Password,
143195
"password", coderdtest.FirstUserParams.Password, // confirm
144196
"trial", "yes",
197+
"firstName", coderdtest.TrialUserParams.FirstName,
198+
"lastName", coderdtest.TrialUserParams.LastName,
199+
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
200+
"jobTitle", coderdtest.TrialUserParams.JobTitle,
201+
"companyName", coderdtest.TrialUserParams.CompanyName,
202+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
145203
}
146204
for i := 0; i < len(matches); i += 2 {
147205
match := matches[i]
@@ -185,6 +243,12 @@ func TestLogin(t *testing.T) {
185243
"password", coderdtest.FirstUserParams.Password,
186244
"password", coderdtest.FirstUserParams.Password, // confirm
187245
"trial", "yes",
246+
"firstName", coderdtest.TrialUserParams.FirstName,
247+
"lastName", coderdtest.TrialUserParams.LastName,
248+
"phoneNumber", coderdtest.TrialUserParams.PhoneNumber,
249+
"jobTitle", coderdtest.TrialUserParams.JobTitle,
250+
"companyName", coderdtest.TrialUserParams.CompanyName,
251+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
188252
}
189253
for i := 0; i < len(matches); i += 2 {
190254
match := matches[i]
@@ -217,6 +281,13 @@ func TestLogin(t *testing.T) {
217281
"--first-user-email", coderdtest.FirstUserParams.Email,
218282
"--first-user-password", coderdtest.FirstUserParams.Password,
219283
"--first-user-trial",
284+
"--first-user-first-name", coderdtest.TrialUserParams.FirstName,
285+
"--first-user-last-name", coderdtest.TrialUserParams.LastName,
286+
"--first-user-phone-number", coderdtest.TrialUserParams.PhoneNumber,
287+
"--first-user-job-title", coderdtest.TrialUserParams.JobTitle,
288+
"--first-user-company-name", coderdtest.TrialUserParams.CompanyName,
289+
"--first-user-country", coderdtest.TrialUserParams.Country,
290+
"--first-user-developers", coderdtest.TrialUserParams.Developers,
220291
)
221292
pty := ptytest.New(t).Attach(inv)
222293
w := clitest.StartWithWaiter(t, inv)
@@ -245,6 +316,14 @@ func TestLogin(t *testing.T) {
245316
"--first-user-email", coderdtest.FirstUserParams.Email,
246317
"--first-user-password", coderdtest.FirstUserParams.Password,
247318
"--first-user-trial",
319+
"--first-user-first-name", coderdtest.TrialUserParams.FirstName,
320+
"--first-user-last-name", coderdtest.TrialUserParams.LastName,
321+
"--first-user-phone-number", coderdtest.TrialUserParams.PhoneNumber,
322+
"--first-user-job-title", coderdtest.TrialUserParams.JobTitle,
323+
"--first-user-company-name", coderdtest.TrialUserParams.CompanyName,
324+
"--first-user-country", coderdtest.TrialUserParams.Country,
325+
"--first-user-developers", coderdtest.TrialUserParams.Developers,
326+
// `developers` and `country` `cliui.Select` automatically selects the first option during tests.
248327
)
249328
pty := ptytest.New(t).Attach(inv)
250329
w := clitest.StartWithWaiter(t, inv)
@@ -299,12 +378,21 @@ func TestLogin(t *testing.T) {
299378
// Validate that we reprompt for matching passwords.
300379
pty.ExpectMatch("Passwords do not match")
301380
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
302-
303381
pty.WriteLine(coderdtest.FirstUserParams.Password)
304382
pty.ExpectMatch("Confirm")
305383
pty.WriteLine(coderdtest.FirstUserParams.Password)
306384
pty.ExpectMatch("trial")
307385
pty.WriteLine("yes")
386+
pty.ExpectMatch("firstName")
387+
pty.WriteLine(coderdtest.TrialUserParams.FirstName)
388+
pty.ExpectMatch("lastName")
389+
pty.WriteLine(coderdtest.TrialUserParams.LastName)
390+
pty.ExpectMatch("phoneNumber")
391+
pty.WriteLine(coderdtest.TrialUserParams.PhoneNumber)
392+
pty.ExpectMatch("jobTitle")
393+
pty.WriteLine(coderdtest.TrialUserParams.JobTitle)
394+
pty.ExpectMatch("companyName")
395+
pty.WriteLine(coderdtest.TrialUserParams.CompanyName)
308396
pty.ExpectMatch("Welcome to Coder")
309397
<-doneChan
310398
})

cli/testdata/coder_login_--help.golden

+21
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,38 @@ USAGE:
66
Authenticate with Coder deployment
77

88
OPTIONS:
9+
--first-user-company-name string, $CODER_FIRST_USER_COMPANY_NAME
10+
Specifies the company name of the user.
11+
12+
--first-user-country string, $CODER_FIRST_USER_COUNTRY
13+
Specifies the country of the user.
14+
15+
--first-user-developers string, $CODER_FIRST_USER_DEVELOPERS
16+
Specifies the number of developers.
17+
918
--first-user-email string, $CODER_FIRST_USER_EMAIL
1019
Specifies an email address to use if creating the first user for the
1120
deployment.
1221

22+
--first-user-first-name string, $CODER_FIRST_USER_FIRST_NAME
23+
Specifies the first name of the user.
24+
1325
--first-user-full-name string, $CODER_FIRST_USER_FULL_NAME
1426
Specifies a human-readable name for the first user of the deployment.
1527

28+
--first-user-job-title string, $CODER_FIRST_USER_JOB_TITLE
29+
Specifies the job title of the user.
30+
31+
--first-user-last-name string, $CODER_FIRST_USER_LAST_NAME
32+
Specifies the last name of the user.
33+
1634
--first-user-password string, $CODER_FIRST_USER_PASSWORD
1735
Specifies a password to use if creating the first user for the
1836
deployment.
1937

38+
--first-user-phone-number string, $CODER_FIRST_USER_PHONE_NUMBER
39+
Specifies the phone number of the user.
40+
2041
--first-user-trial bool, $CODER_FIRST_USER_TRIAL
2142
Specifies whether a trial license should be provisioned for the Coder
2243
deployment or not.

coderd/coderdtest/coderdtest.go

+10
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,16 @@ var FirstUserParams = codersdk.CreateFirstUserRequest{
651651
Name: "Test User",
652652
}
653653

654+
var TrialUserParams = codersdk.CreateFirstUserTrialInfo{
655+
FirstName: "John",
656+
LastName: "Doe",
657+
PhoneNumber: "9999999999",
658+
JobTitle: "Engineer",
659+
CompanyName: "Acme Inc",
660+
Country: "United States",
661+
Developers: "10-50",
662+
}
663+
654664
// CreateFirstUser creates a user with preset credentials and authenticates
655665
// with the passed in codersdk client.
656666
func CreateFirstUser(t testing.TB, client *codersdk.Client) codersdk.CreateFirstUserResponse {

0 commit comments

Comments
 (0)