Skip to content

Commit 931118f

Browse files
allmightyspiffGitHub Enterprise
authored andcommitted
Merge pull request #861 from SoftLayer/issues851
Add VPN status to user list and user detail
2 parents c21638e + 9c2a27f commit 931118f

18 files changed

Lines changed: 239 additions & 157 deletions

.secrets.baseline

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "plugin/i18n/v1Resources/|plugin/i18n/v2Resources/|(.*test.*)|(vendor)|(go.sum)|bin/|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2024-06-06T23:07:46Z",
6+
"generated_at": "2024-07-22T22:42:28Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -296,15 +296,15 @@
296296
"hashed_secret": "bde54745b8d37fae50e777aa46ecae8f40ee2fcf",
297297
"is_secret": false,
298298
"is_verified": false,
299-
"line_number": 53,
299+
"line_number": 35,
300300
"type": "Secret Keyword",
301301
"verified_result": null
302302
},
303303
{
304304
"hashed_secret": "0e7312e100f7e0691b1deb8ffbe98766a1cdb902",
305305
"is_secret": false,
306306
"is_verified": false,
307-
"line_number": 91,
307+
"line_number": 102,
308308
"type": "Secret Keyword",
309309
"verified_result": null
310310
}

plugin/commands/user/details.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func (cmd *DetailsCommand) Run(args []string) error {
7878
logins := cmd.Logins
7979
events := cmd.Events
8080

81-
object_mask := "userStatus[name],parent[id,username],apiAuthenticationKeys[authenticationKey]"
81+
object_mask := "userStatus[name],parent[id,username],apiAuthenticationKeys[authenticationKey],sslVpnAllowedFlag"
8282
user, err := cmd.UserManager.GetUser(id, object_mask)
8383
userInfo.User = user
8484
if err != nil {
@@ -249,8 +249,6 @@ func baseUserPrint(user datatypes.User_Customer, keys bool, ui terminal.UI) {
249249
if user.UserStatus != nil {
250250
table.Add(T("Status"), utils.FormatStringPointer(user.UserStatus.Name))
251251
}
252-
253-
table.Add(T("PPTP VPN"), utils.FormatBoolPointer(user.PptpVpnAllowedFlag))
254252
table.Add(T("SSL VPN"), utils.FormatBoolPointer(user.SslVpnAllowedFlag))
255253

256254
if len(user.SuccessfulLogins) != 0 {

plugin/commands/user/list.go

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package user
22

33
import (
4+
"fmt"
5+
"strings"
6+
47
"github.com/spf13/cobra"
58
"github.ibm.com/SoftLayer/softlayer-cli/plugin/errors"
69
. "github.ibm.com/SoftLayer/softlayer-cli/plugin/i18n"
@@ -20,6 +23,19 @@ type ListCommand struct {
2023
Column []string
2124
}
2225

26+
var maskMap = map[string]string{
27+
"id": "id",
28+
"username": "username",
29+
"email": "email",
30+
"displayName": "displayName",
31+
"status": "userStatus.name",
32+
"hardwareCount": "hardwareCount",
33+
"virtualGuestCount": "virtualGuestCount",
34+
"2FA": "externalBindingCount",
35+
"classicAPIKey": "apiAuthenticationKeyCount",
36+
"vpn": "sslVpnAllowedFlag",
37+
}
38+
2339
func NewListCommand(sl *metadata.SoftlayerCommand) (cmd *ListCommand) {
2440
thisCmd := &ListCommand{
2541
SoftlayerCommand: sl,
@@ -35,29 +51,24 @@ func NewListCommand(sl *metadata.SoftlayerCommand) (cmd *ListCommand) {
3551
},
3652
}
3753

38-
cobraCmd.Flags().StringSliceVar(&thisCmd.Column, "column", []string{}, T("Column to display. options are: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. This option can be specified multiple times"))
54+
columns := ""
55+
for key, _ := range maskMap {
56+
columns = fmt.Sprintf("%s %s,", columns, key)
57+
}
58+
columns = strings.TrimSuffix(columns, ",")
59+
subs := map[string]interface{}{"Columns": columns}
60+
cobraCmd.Flags().StringSliceVar(&thisCmd.Column, "column", []string{},
61+
T("Column to display. options are: {{.Columns}}. This option can be specified multiple times", subs))
3962

4063
thisCmd.Command = cobraCmd
4164
return thisCmd
4265
}
4366

44-
var maskMap = map[string]string{
45-
"id": "id",
46-
"username": "username",
47-
"email": "email",
48-
"displayName": "displayName",
49-
"status": "userStatus.name",
50-
"hardwareCount": "hardwareCount",
51-
"virtualGuestCount": "virtualGuestCount",
52-
"2FA": "externalBindingCount",
53-
"classicAPIKey": "apiAuthenticationKeyCount",
54-
}
55-
5667
func (cmd *ListCommand) Run(args []string) error {
5768

5869
columns := cmd.Column
5970

60-
defaultColumns := []string{"id", "username", "email", "displayName", "2FA", "classicAPIKey"}
71+
defaultColumns := []string{"id", "username", "email", "displayName", "2FA", "classicAPIKey", "vpn"}
6172
optionalColumns := []string{"status", "hardwareCount", "virtualGuestCount"}
6273

6374
showColumns, err := utils.ValidateColumns2("", columns, defaultColumns, optionalColumns, []string{})
@@ -89,6 +100,7 @@ func (cmd *ListCommand) Run(args []string) error {
89100

90101
values["2FA"] = utils.ReplaceUIntPointerValue(user.ExternalBindingCount, NO_ZERO_VALUE)
91102
values["classicAPIKey"] = utils.ReplaceUIntPointerValue(user.ApiAuthenticationKeyCount, NO_ZERO_VALUE)
103+
values["vpn"] = utils.FormatBoolPointerToYN(user.SslVpnAllowedFlag)
92104

93105
if user.UserStatus != nil {
94106
values["status"] = utils.FormatStringPointer(user.UserStatus.Name)

plugin/commands/user/list_test.go

Lines changed: 61 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,92 @@
11
package user_test
22

33
import (
4-
"errors"
5-
64
"github.com/IBM-Cloud/ibm-cloud-cli-sdk/testhelpers/terminal"
75
. "github.com/onsi/ginkgo/v2"
86
. "github.com/onsi/gomega"
9-
"github.com/softlayer/softlayer-go/datatypes"
107
"github.com/softlayer/softlayer-go/session"
11-
"github.com/softlayer/softlayer-go/sl"
8+
"strings"
129

1310
"github.ibm.com/SoftLayer/softlayer-cli/plugin/commands/user"
1411
"github.ibm.com/SoftLayer/softlayer-cli/plugin/metadata"
1512
"github.ibm.com/SoftLayer/softlayer-cli/plugin/testhelpers"
1613
)
1714

18-
var _ = Describe("User List", func() {
15+
var _ = Describe("sl user list", func() {
1916
var (
20-
fakeUI *terminal.FakeUI
21-
fakeUserManager *testhelpers.FakeUserManager
22-
cliCommand *user.ListCommand
23-
fakeSession *session.Session
24-
slCommand *metadata.SoftlayerCommand
17+
fakeUI *terminal.FakeUI
18+
// fakeUserManager *testhelpers.FakeUserManager
19+
cliCommand *user.ListCommand
20+
fakeSession *session.Session
21+
slCommand *metadata.SoftlayerCommand
22+
fakeHandler *testhelpers.FakeTransportHandler
2523
)
2624
BeforeEach(func() {
2725
fakeUI = terminal.NewFakeUI()
28-
fakeUserManager = new(testhelpers.FakeUserManager)
29-
fakeSession = testhelpers.NewFakeSoftlayerSession([]string{})
26+
// fakeUserManager = new(testhelpers.FakeUserManager)
27+
fakeSession = testhelpers.NewFakeSoftlayerSession(nil)
28+
fakeHandler = testhelpers.GetSessionHandler(fakeSession)
3029
slCommand = metadata.NewSoftlayerCommand(fakeUI, fakeSession)
3130
cliCommand = user.NewListCommand(slCommand)
3231
cliCommand.Command.PersistentFlags().Var(cliCommand.OutputFlag, "output", "--output=JSON for json output.")
33-
cliCommand.UserManager = fakeUserManager
34-
35-
testListUser := []datatypes.User_Customer{
36-
datatypes.User_Customer{
37-
Id: sl.Int(5555),
38-
Username: sl.String("ATestUser"),
39-
Email: sl.String("user2@email.com"),
40-
DisplayName: sl.String("DisplayedName"),
41-
ExternalBindingCount: sl.Uint(123),
42-
ApiAuthenticationKeyCount: sl.Uint(123456),
43-
UserStatus: &datatypes.User_Customer_Status{
44-
Name: sl.String("ACTIVE"),
45-
},
46-
},
47-
datatypes.User_Customer{
48-
Id: sl.Int(5556),
49-
Username: sl.String("ATestUser2"),
50-
Email: sl.String("user2@email.com"),
51-
DisplayName: sl.String("DisplayedName2"),
52-
ExternalBindingCount: sl.Uint(1234),
53-
ApiAuthenticationKeyCount: sl.Uint(1234567),
54-
},
55-
}
56-
fakeUserManager.ListUsersReturns(testListUser, nil)
32+
})
33+
AfterEach(func() {
34+
// Clear API call logs and any errors that might have been set after every test
35+
fakeHandler.ClearApiCallLogs()
36+
fakeHandler.ClearErrors()
5737
})
5838

59-
Describe("user list ", func() {
60-
Context("user list with unknown column", func() {
61-
It("return error", func() {
62-
err := testhelpers.RunCobraCommand(cliCommand.Command, "--column", "noExist")
63-
Expect(err).To(HaveOccurred())
64-
Expect(err.Error()).To(ContainSubstring("Incorrect Usage: --column noExist is not supported."))
65-
})
39+
Describe("Usage Errors ", func() {
40+
It("return error", func() {
41+
err := testhelpers.RunCobraCommand(cliCommand.Command, "--column", "noExist")
42+
Expect(err).To(HaveOccurred())
43+
Expect(err.Error()).To(ContainSubstring("Incorrect Usage: --column noExist is not supported."))
6644
})
45+
})
6746

68-
Context("user list fatal error", func() {
69-
It("return error", func() {
70-
fakeUserManager.ListUsersReturns([]datatypes.User_Customer{}, errors.New("Internal server error"))
71-
err := testhelpers.RunCobraCommand(cliCommand.Command)
72-
Expect(err).To(HaveOccurred())
73-
Expect(err.Error()).To(ContainSubstring("Failed to list users."))
74-
})
47+
Describe("API Errors", func() {
48+
It("SoftLayer_Account::getUsers API Error", func() {
49+
fakeHandler.AddApiError("SoftLayer_Account", "getUsers", 500, "Internal Server Error")
50+
err := testhelpers.RunCobraCommand(cliCommand.Command)
51+
Expect(err).To(HaveOccurred())
52+
Expect(err.Error()).To(ContainSubstring("Failed to list users."))
7553
})
54+
})
7655

77-
Context("user list", func() {
78-
It("return users list", func() {
79-
err := testhelpers.RunCobraCommand(cliCommand.Command)
80-
Expect(err).NotTo(HaveOccurred())
81-
Expect(fakeUI.Outputs()).To(ContainSubstring("id username email displayName 2FA classicAPIKey"))
82-
Expect(fakeUI.Outputs()).To(ContainSubstring("5555 ATestUser user2@email.com DisplayedName yes yes"))
83-
Expect(fakeUI.Outputs()).To(ContainSubstring("5556 ATestUser2 user2@email.com DisplayedName2 yes yes"))
84-
})
56+
Describe("Happy Path", func() {
57+
It("return users list", func() {
58+
err := testhelpers.RunCobraCommand(cliCommand.Command)
59+
Expect(err).NotTo(HaveOccurred())
60+
// Remove whitespace to make testing output a bit less rigid
61+
trimmed := strings.ReplaceAll(fakeUI.Outputs(), " ", "")
62+
Expect(trimmed).To(ContainSubstring("idusernameemaildisplayName2FAclassicAPIKeyvpn"))
63+
Expect(trimmed).To(ContainSubstring("1468361IBM27821sdfasdfasd@one.comazzz---"))
64+
Expect(trimmed).To(ContainSubstring("1468362IBM27832sdfasdfasd@two.comaccc-yesYes"))
65+
Expect(trimmed).To(ContainSubstring("1468363IBM27843sdfasdfasd@three.comabc--No"))
8566
})
86-
87-
Context("user list with column", func() {
88-
It("return users list", func() {
89-
err := testhelpers.RunCobraCommand(cliCommand.Command, "--column", "username")
90-
Expect(err).NotTo(HaveOccurred())
91-
Expect(fakeUI.Outputs()).To(ContainSubstring("username"))
92-
Expect(fakeUI.Outputs()).To(ContainSubstring("ATestUser"))
93-
Expect(fakeUI.Outputs()).To(ContainSubstring("ATestUser2"))
94-
})
67+
It("return users list simple columns", func() {
68+
err := testhelpers.RunCobraCommand(cliCommand.Command, "--column=id", "--column=displayName")
69+
Expect(err).NotTo(HaveOccurred())
70+
// Remove whitespace to make testing output a bit less rigid
71+
trimmed := strings.ReplaceAll(fakeUI.Outputs(), " ", "")
72+
Expect(trimmed).To(ContainSubstring("iddisplayName"))
73+
Expect(trimmed).To(ContainSubstring("1468361azzz"))
74+
Expect(trimmed).To(ContainSubstring("1468362accc"))
75+
Expect(trimmed).To(ContainSubstring("1468363abc"))
9576
})
96-
97-
Context("user list in format json", func() {
98-
It("return users list", func() {
99-
err := testhelpers.RunCobraCommand(cliCommand.Command, "--output", "json")
100-
Expect(err).NotTo(HaveOccurred())
101-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"apiAuthenticationKeyCount": 123456,`))
102-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"displayName": "DisplayedName",`))
103-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"email": "user2@email.com",`))
104-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"externalBindingCount": 123,`))
105-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"id": 5555,`))
106-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"userStatus": {`))
107-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"name": "ACTIVE"`))
108-
Expect(fakeUI.Outputs()).To(ContainSubstring(`},`))
109-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"username": "ATestUser"`))
110-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"apiAuthenticationKeyCount": 1234567,`))
111-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"displayName": "DisplayedName2",`))
112-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"email": "user2@email.com",`))
113-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"externalBindingCount": 1234,`))
114-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"id": 5556,`))
115-
Expect(fakeUI.Outputs()).To(ContainSubstring(`"username": "ATestUser2"`))
116-
})
77+
It("return users list with just username", func() {
78+
err := testhelpers.RunCobraCommand(cliCommand.Command, "--column", "username")
79+
Expect(err).NotTo(HaveOccurred())
80+
Expect(fakeUI.Outputs()).To(ContainSubstring("username"))
81+
Expect(fakeUI.Outputs()).To(ContainSubstring("IBM27821"))
82+
Expect(fakeUI.Outputs()).To(ContainSubstring("IBM27832"))
83+
})
84+
It("return users list with json output", func() {
85+
err := testhelpers.RunCobraCommand(cliCommand.Command, "--output", "json")
86+
Expect(err).NotTo(HaveOccurred())
87+
Expect(fakeUI.Outputs()).To(ContainSubstring(`"email": "sdfasdfasd@three.com",`))
88+
Expect(fakeUI.Outputs()).To(ContainSubstring(`"displayName": "azzz",`))
89+
Expect(fakeUI.Outputs()).To(ContainSubstring(`"apiAuthenticationKeyCount": 1,`))
11790
})
11891
})
11992
})

plugin/i18n/v2Resources/active.de_DE.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,8 +1325,8 @@
13251325
"Column to display. [Options are: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [default: id,hostname,domain,primary_ip,backend_ip,power_state].": {
13261326
"other": "Anzuzeigende Spalte. [Optionen: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [Standardwert: id, hostname, domain, primary_ip, backend_ip, power_state]"
13271327
},
1328-
"Column to display. options are: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. This option can be specified multiple times": {
1329-
"other": "Anzuzeigende Spalte. Optionen: id,username,email,displayName,2FA, classicAPIKey, status, hardwareCount, virtualGuestCount. Diese Option kann mehrmals angegeben werden."
1328+
"Column to display. options are: {{.Columns}}. This option can be specified multiple times": {
1329+
"other": "Anzuzeigende Spalte. Optionen: {{.Columns}}. Diese Option kann mehrmals angegeben werden."
13301330
},
13311331
"Column to sort by": {
13321332
"other": "Spalte, nach der sortiert werden soll"

plugin/i18n/v2Resources/active.en-US.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,8 +1328,8 @@
13281328
"Column to display. [Options are: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [default: id,hostname,domain,primary_ip,backend_ip,power_state].": {
13291329
"other": "Column to display. [Options are: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [default: id,hostname,domain,primary_ip,backend_ip,power_state]."
13301330
},
1331-
"Column to display. options are: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. This option can be specified multiple times": {
1332-
"other": "Column to display. options are: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. This option can be specified multiple times"
1331+
"Column to display. options are: {{.Columns}}. This option can be specified multiple times": {
1332+
"other": "Column to display. options are: {{.Columns}}. This option can be specified multiple times"
13331333
},
13341334
"Column to sort by": {
13351335
"other": "Column to sort by"

plugin/i18n/v2Resources/active.es_ES.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,8 +1325,8 @@
13251325
"Column to display. [Options are: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [default: id,hostname,domain,primary_ip,backend_ip,power_state].": {
13261326
"other": "Columna a visualizar. [Las opciones son: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [default: id,hostname,domain,primary_ip,backend_ip,power_state]."
13271327
},
1328-
"Column to display. options are: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. This option can be specified multiple times": {
1329-
"other": "Columna a visualizar. Las opciones son: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. Esta opción se puede especificar varias veces"
1328+
"Column to display. options are: {{.Columns}}. This option can be specified multiple times": {
1329+
"other": "Columna a visualizar. Las opciones son: {{.Columns}}. Esta opción se puede especificar varias veces"
13301330
},
13311331
"Column to sort by": {
13321332
"other": "Columna por la que se ordena"

plugin/i18n/v2Resources/active.fr_FR.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,8 +1325,8 @@
13251325
"Column to display. [Options are: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [default: id,hostname,domain,primary_ip,backend_ip,power_state].": {
13261326
"other": "Colonne à afficher. [Les options sont : guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [par défaut : id,hostname,domain,primary_ip,backend_ip,power_state]."
13271327
},
1328-
"Column to display. options are: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. This option can be specified multiple times": {
1329-
"other": "Colonne à afficher. Options : id, username, email, displayName, 2FA, classicAPIKey, statut, hardwareCount, virtualGuestCount. Cette option peut être indiquée plusieurs fois."
1328+
"Column to display. options are: {{.Columns}}. This option can be specified multiple times": {
1329+
"other": "Colonne à afficher. Options : {{.Columns}}. Cette option peut être indiquée plusieurs fois."
13301330
},
13311331
"Column to sort by": {
13321332
"other": "Colonne de tri"

plugin/i18n/v2Resources/active.it_IT.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,8 +1325,8 @@
13251325
"Column to display. [Options are: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [default: id,hostname,domain,primary_ip,backend_ip,power_state].": {
13261326
"other": "Colonna da visualizzare. [Le opzioni sono: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [valore predefinito: id,hostname,domain,primary_ip,backend_ip,power_state]."
13271327
},
1328-
"Column to display. options are: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. This option can be specified multiple times": {
1329-
"other": "Colonna da visualizzare. Le opzioni sono: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. Questa opzione può essere specificata più volte."
1328+
"Column to display. options are: {{.Columns}}. This option can be specified multiple times": {
1329+
"other": "Colonna da visualizzare. Le opzioni sono: {{.Columns}}. Questa opzione può essere specificata più volte."
13301330
},
13311331
"Column to sort by": {
13321332
"other": "Colonna in base alla quale ordinare"

plugin/i18n/v2Resources/active.ja_JP.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,8 +1325,8 @@
13251325
"Column to display. [Options are: guid, cpu, memory, datacenter, primary_ip, backend_ip, created_by, power_state, tags] [default: id,hostname,domain,primary_ip,backend_ip,power_state].": {
13261326
"other": "表示する列。 [オプション: guid、cpu、memory、datacenter、primary_ip、backend_ip、created_by、power_state、tags] [デフォルト: id、hostname、domain、primary_ip、backend_ip、power_state]。"
13271327
},
1328-
"Column to display. options are: id,username,email,displayName,2FA,classicAPIKey,status,hardwareCount,virtualGuestCount. This option can be specified multiple times": {
1329-
"other": "表示する列。 オプション: id,username,email,displayName,2FA、classicAPIKey、status、hardwareCount、virtualGuestCount。 このオプションは複数回指定できます"
1328+
"Column to display. options are: {{.Columns}}. This option can be specified multiple times": {
1329+
"other": "表示する列。 オプション: {{.Columns}}。 このオプションは複数回指定できます"
13301330
},
13311331
"Column to sort by": {
13321332
"other": "ソート基準の列"

0 commit comments

Comments
 (0)