You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli

Package Overview
Dependencies
Versions
174
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli - go Package Compare versions

Comparing version
v0.9.1
to
v0.10.0
+39
docs/stackit_auth_logout.md
## stackit auth logout
Logs the user account out of the STACKIT CLI
### Synopsis
Logs the user account out of the STACKIT CLI.
```
stackit auth logout [flags]
```
### Examples
```
Log out of the STACKIT CLI.
$ stackit auth logout
```
### Options
```
-h, --help Help for "stackit auth logout"
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit auth](./stackit_auth.md) - Authenticates the STACKIT CLI
## stackit dns zone clone
Clones a DNS zone
### Synopsis
Clones an existing DNS zone with all record sets to a new zone with a different name.
```
stackit dns zone clone [flags]
```
### Examples
```
Clones a DNS zone with ID "xxx" to a new zone with DNS name "www.my-zone.com"
$ stackit dns zone clone xxx --dns-name www.my-zone.com
Clones a DNS zone with ID "xxx" to a new zone with DNS name "www.my-zone.com" and display name "new-zone"
$ stackit dns zone clone xxx --dns-name www.my-zone.com --name new-zone
Clones a DNS zone with ID "xxx" to a new zone with DNS name "www.my-zone.com" and adjust records "true"
$ stackit dns zone clone xxx --dns-name www.my-zone.com --adjust-records
```
### Options
```
--adjust-records Sets content and replaces the DNS name of the original zone with the new DNS name of the cloned zone
--description string New description for the cloned zone
--dns-name string Fully qualified domain name of the new DNS zone to clone
-h, --help Help for "stackit dns zone clone"
--name string User given new name for the cloned zone
```
### Options inherited from parent commands
```
-y, --assume-yes If set, skips all confirmation prompts
--async If set, runs the command asynchronously
-o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
-p, --project-id string Project ID
--verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
```
### SEE ALSO
* [stackit dns zone](./stackit_dns_zone.md) - Provides functionality for DNS zones
package logout
import (
"fmt"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/spf13/cobra"
)
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "logout",
Short: "Logs the user account out of the STACKIT CLI",
Long: "Logs the user account out of the STACKIT CLI.",
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Log out of the STACKIT CLI.`,
"$ stackit auth logout"),
),
RunE: func(cmd *cobra.Command, args []string) error {
err := auth.LogoutUser()
if err != nil {
return fmt.Errorf("log out failed: %w", err)
}
p.Info("Successfully logged out of the STACKIT CLI.\n")
return nil
},
}
return cmd
}
package clone
import (
"context"
"testing"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/stackitcloud/stackit-sdk-go/services/dns"
)
var projectIdFlag = globalflags.ProjectIdFlag
type testCtxKey struct{}
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
var testClient = &dns.APIClient{}
var testProjectId = uuid.NewString()
var testZoneId = uuid.NewString()
func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testZoneId,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
projectIdFlag: testProjectId,
nameFlag: "example",
dnsNameFlag: "example.com",
descriptionFlag: "Example",
adjustRecordsFlag: "false",
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
Name: utils.Ptr("example"),
DnsName: utils.Ptr("example.com"),
Description: utils.Ptr("Example"),
AdjustRecords: utils.Ptr(false),
ZoneId: testZoneId,
}
for _, mod := range mods {
mod(model)
}
return model
}
func fixtureRequest(mods ...func(request *dns.ApiCloneZoneRequest)) dns.ApiCloneZoneRequest {
request := testClient.CloneZone(testCtx, testProjectId, testZoneId)
request = request.CloneZonePayload(dns.CloneZonePayload{
Name: utils.Ptr("example"),
DnsName: utils.Ptr("example.com"),
Description: utils.Ptr("Example"),
AdjustRecords: utils.Ptr(false),
})
for _, mod := range mods {
mod(&request)
}
return request
}
func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flag values",
argValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: false,
},
{
description: "required fields only",
argValues: []string{testZoneId},
flagValues: map[string]string{
projectIdFlag: testProjectId,
dnsNameFlag: "example.com",
},
isValid: true,
expectedModel: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
DnsName: utils.Ptr("example.com"),
ZoneId: testZoneId,
},
},
{
description: "project id missing",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, projectIdFlag)
}),
isValid: false,
},
{
description: "project id invalid 1",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = ""
}),
isValid: false,
},
{
description: "project id invalid 2",
argValues: fixtureArgValues(),
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
flagValues[projectIdFlag] = "invalid-uuid"
}),
isValid: false,
},
{
description: "zone id invalid 1",
argValues: []string{""},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "zone id invalid 2",
argValues: []string{"invalid-uuid"},
flagValues: fixtureFlagValues(),
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
p := print.NewPrinter()
cmd := NewCmd(p)
err := globalflags.Configure(cmd.Flags())
if err != nil {
t.Fatalf("configure global flags: %v", err)
}
for flag, value := range tt.flagValues {
err := cmd.Flags().Set(flag, value)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
}
}
err = cmd.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}
err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}
model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing flags: %v", err)
}
if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestBuildRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
isValid bool
expectedRequest dns.ApiCloneZoneRequest
}{
{
description: "base",
model: fixtureInputModel(),
isValid: true,
expectedRequest: fixtureRequest(),
},
{
description: "required fields only",
model: &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
ProjectId: testProjectId,
Verbosity: globalflags.VerbosityDefault,
},
DnsName: utils.Ptr("example.com"),
ZoneId: testZoneId,
},
expectedRequest: testClient.CloneZone(testCtx, testProjectId, testZoneId).
CloneZonePayload(dns.CloneZonePayload{
DnsName: utils.Ptr("example.com"),
}),
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
package clone
import (
"context"
"encoding/json"
"fmt"
"github.com/goccy/go-yaml"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
"github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/client"
dnsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/utils"
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
"github.com/spf13/cobra"
"github.com/stackitcloud/stackit-sdk-go/services/dns"
"github.com/stackitcloud/stackit-sdk-go/services/dns/wait"
)
const (
nameFlag = "name"
dnsNameFlag = "dns-name"
descriptionFlag = "description"
adjustRecordsFlag = "adjust-records"
zoneIdArg = "ZONE_ID"
)
type inputModel struct {
*globalflags.GlobalFlagModel
Name *string
DnsName *string
Description *string
AdjustRecords *bool
ZoneId string
}
func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: "clone",
Short: "Clones a DNS zone",
Long: "Clones an existing DNS zone with all record sets to a new zone with a different name.",
Args: args.SingleArg(zoneIdArg, utils.ValidateUUID),
Example: examples.Build(
examples.NewExample(
`Clones a DNS zone with ID "xxx" to a new zone with DNS name "www.my-zone.com"`,
"$ stackit dns zone clone xxx --dns-name www.my-zone.com"),
examples.NewExample(
`Clones a DNS zone with ID "xxx" to a new zone with DNS name "www.my-zone.com" and display name "new-zone"`,
"$ stackit dns zone clone xxx --dns-name www.my-zone.com --name new-zone"),
examples.NewExample(
`Clones a DNS zone with ID "xxx" to a new zone with DNS name "www.my-zone.com" and adjust records "true"`,
"$ stackit dns zone clone xxx --dns-name www.my-zone.com --adjust-records"),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}
// Configure API client
apiClient, err := client.ConfigureClient(p)
if err != nil {
return err
}
zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient, model.ProjectId, model.ZoneId)
if err != nil {
p.Debug(print.ErrorLevel, "get zone name: %v", err)
zoneLabel = model.ZoneId
}
if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to clone the zone %q?", zoneLabel)
err = p.PromptForConfirmation(prompt)
if err != nil {
return err
}
}
// Call API
req := buildRequest(ctx, model, apiClient)
resp, err := req.Execute()
if err != nil {
return fmt.Errorf("clone DNS zone: %w", err)
}
zoneId := *resp.Zone.Id
// Wait for async operation, if async mode not enabled
if !model.Async {
s := spinner.New(p)
s.Start("Cloning zone")
_, err = wait.CreateZoneWaitHandler(ctx, apiClient, model.ProjectId, zoneId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("wait for DNS zone cloning: %w", err)
}
s.Stop()
}
return outputResult(p, model, zoneLabel, resp)
},
}
configureFlags(cmd)
return cmd
}
func configureFlags(cmd *cobra.Command) {
cmd.Flags().String(nameFlag, "", "User given new name for the cloned zone")
cmd.Flags().String(dnsNameFlag, "", "Fully qualified domain name of the new DNS zone to clone")
cmd.Flags().String(descriptionFlag, "", "New description for the cloned zone")
cmd.Flags().Bool(adjustRecordsFlag, false, "Sets content and replaces the DNS name of the original zone with the new DNS name of the cloned zone")
err := flags.MarkFlagsRequired(cmd, dnsNameFlag)
cobra.CheckErr(err)
}
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
zoneId := inputArgs[0]
globalFlags := globalflags.Parse(p, cmd)
if globalFlags.ProjectId == "" {
return nil, &errors.ProjectIdError{}
}
model := inputModel{
GlobalFlagModel: globalFlags,
Name: flags.FlagToStringPointer(p, cmd, nameFlag),
DnsName: flags.FlagToStringPointer(p, cmd, dnsNameFlag),
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
AdjustRecords: flags.FlagToBoolPointer(p, cmd, adjustRecordsFlag),
ZoneId: zoneId,
}
if p.IsVerbosityDebug() {
modelStr, err := print.BuildDebugStrFromInputModel(model)
if err != nil {
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
} else {
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
}
}
return &model, nil
}
func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiCloneZoneRequest {
req := apiClient.CloneZone(ctx, model.ProjectId, model.ZoneId)
req = req.CloneZonePayload(dns.CloneZonePayload{
Name: model.Name,
DnsName: model.DnsName,
Description: model.Description,
AdjustRecords: model.AdjustRecords,
})
return req
}
func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *dns.ZoneResponse) error {
switch model.OutputFormat {
case print.JSONOutputFormat:
details, err := json.MarshalIndent(resp, "", " ")
if err != nil {
return fmt.Errorf("marshal DNS zone: %w", err)
}
p.Outputln(string(details))
return nil
case print.YAMLOutputFormat:
details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true))
if err != nil {
return fmt.Errorf("marshal DNS zone: %w", err)
}
p.Outputln(string(details))
return nil
default:
operationState := "Cloned"
if model.Async {
operationState = "Triggered cloning of"
}
p.Outputf("%s zone for project %q. Zone ID: %s\n", operationState, projectLabel, *resp.Zone.Id)
return nil
}
}
+1
-1

@@ -16,5 +16,5 @@ name: Renovate

- name: Self-hosted Renovate
uses: renovatebot/github-action@v40.1.12
uses: renovatebot/github-action@v40.2.3
with:
configurationFile: .github/renovate.json
token: ${{ secrets.RENOVATE_TOKEN }}

@@ -34,2 +34,3 @@ ## stackit auth

* [stackit auth login](./stackit_auth_login.md) - Logs in to the STACKIT CLI
* [stackit auth logout](./stackit_auth_logout.md) - Logs the user account out of the STACKIT CLI

@@ -32,2 +32,3 @@ ## stackit dns zone

* [stackit dns](./stackit_dns.md) - Provides functionality for DNS
* [stackit dns zone clone](./stackit_dns_zone_clone.md) - Clones a DNS zone
* [stackit dns zone create](./stackit_dns_zone_create.md) - Creates a DNS zone

@@ -34,0 +35,0 @@ * [stackit dns zone delete](./stackit_dns_zone_delete.md) - Deletes a DNS zone

+2
-2

@@ -29,4 +29,4 @@ module github.com/stackitcloud/stackit-cli

github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.2.0
github.com/stackitcloud/stackit-sdk-go/services/ske v0.18.0
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.3.0
github.com/stackitcloud/stackit-sdk-go/services/ske v0.19.0
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.4.0
github.com/zalando/go-keyring v0.2.5

@@ -33,0 +33,0 @@ golang.org/x/mod v0.19.0

+4
-4

@@ -162,6 +162,6 @@ github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=

github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.2.0/go.mod h1:z6XdA+ndaWzcPW/P0QrUIcTXJzKlajxgGZ5+EwXNS+c=
github.com/stackitcloud/stackit-sdk-go/services/ske v0.18.0 h1:gxbOYBpHBLMuzltbOGz7OwRFtjCZ9X56a2apsPP/8uU=
github.com/stackitcloud/stackit-sdk-go/services/ske v0.18.0/go.mod h1:0fFs4R7kg+gU7FNAIzzFvlCZJz6gyZ8CFhbK3eSrAwQ=
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.3.0 h1:M6tcXUMNM6XMfHVQeQzB6IjfPdAxnZar3YD+YstRStc=
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.3.0/go.mod h1:Qnn+06i21XtagtMQ4cTwOCR3OLnXX+t1n+Vf/HH49Yw=
github.com/stackitcloud/stackit-sdk-go/services/ske v0.19.0 h1:vmkfa26HO1VA40pKPNnYMHkcNMxBEWAdYbX+5LVIo48=
github.com/stackitcloud/stackit-sdk-go/services/ske v0.19.0/go.mod h1:0fFs4R7kg+gU7FNAIzzFvlCZJz6gyZ8CFhbK3eSrAwQ=
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.4.0 h1:Ly8dDjjggO7udf5kyyQU6Lm6UPoWzJVJONYeaIDkFVM=
github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.4.0/go.mod h1:Qnn+06i21XtagtMQ4cTwOCR3OLnXX+t1n+Vf/HH49Yw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

@@ -168,0 +168,0 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=

@@ -6,2 +6,3 @@ package auth

"github.com/stackitcloud/stackit-cli/internal/cmd/auth/login"
"github.com/stackitcloud/stackit-cli/internal/cmd/auth/logout"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"

@@ -28,3 +29,4 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/print"

cmd.AddCommand(login.NewCmd(p))
cmd.AddCommand(logout.NewCmd(p))
cmd.AddCommand(activateserviceaccount.NewCmd(p))
}

@@ -137,10 +137,2 @@ package describe

table.AddSeparator()
if database.CreateDate != nil {
table.AddRow("CREATE DATE", *database.CreateDate)
table.AddSeparator()
}
if database.Collation != nil {
table.AddRow("COLLATION", *database.Collation)
table.AddSeparator()
}
if database.Options != nil {

@@ -151,6 +143,2 @@ if database.Options.CompatibilityLevel != nil {

}
if database.Options.IsEncrypted != nil {
table.AddRow("IS ENCRYPTED", *database.Options.IsEncrypted)
table.AddSeparator()
}
if database.Options.Owner != nil {

@@ -160,4 +148,4 @@ table.AddRow("OWNER", *database.Options.Owner)

}
if database.Options.UserAccess != nil {
table.AddRow("USER ACCESS", *database.Options.UserAccess)
if database.Options.CollationName != nil {
table.AddRow("COLLATION", *database.Options.CollationName)
}

@@ -164,0 +152,0 @@ }

@@ -79,5 +79,5 @@ package delete

err = auth.DeleteProfileFromKeyring(model.Profile)
err = auth.DeleteProfileAuth(model.Profile)
if err != nil {
return fmt.Errorf("delete profile from keyring: %w", err)
return fmt.Errorf("delete profile authentication: %w", err)
}

@@ -84,0 +84,0 @@

@@ -61,3 +61,3 @@ package delete

if !model.AssumeYes {
prompt := fmt.Sprintf("Are you sure you want to delete zone %s? (This cannot be undone)", zoneLabel)
prompt := fmt.Sprintf("Are you sure you want to delete zone %q? (This cannot be undone)", zoneLabel)
err = p.PromptForConfirmation(prompt)

@@ -64,0 +64,0 @@ if err != nil {

package zone
import (
"github.com/stackitcloud/stackit-cli/internal/cmd/dns/zone/clone"
"github.com/stackitcloud/stackit-cli/internal/cmd/dns/zone/create"

@@ -34,2 +35,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/dns/zone/delete"

cmd.AddCommand(delete.NewCmd(p))
cmd.AddCommand(clone.NewCmd(p))
}
package auth
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
"github.com/zalando/go-keyring"
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
)

@@ -320,3 +316,3 @@

err = deleteAuthFieldProfile(tt.activeProfile)
err = deleteProfileFiles(tt.activeProfile)
if err != nil {

@@ -472,2 +468,152 @@ t.Errorf("Post-test cleanup failed: remove profile \"%s\": %v. Please remove it manually", tt.activeProfile, err)

func TestDeleteAuthField(t *testing.T) {
tests := []struct {
description string
keyringFails bool
key authFieldKey
noKey bool
}{
{
description: "base",
key: "test-field-1",
},
{
description: "key doesnt exist",
key: "doesnt-exist",
noKey: true,
},
{
description: "keyring fails",
keyringFails: true,
key: "test-field-1",
},
{
description: "keyring fails, no key exists",
keyringFails: true,
noKey: true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
if !tt.keyringFails {
keyring.MockInit()
} else {
keyring.MockInitWithError(fmt.Errorf("keyring unavailable for testing"))
}
// Append random string to auth field key and value to avoid conflicts
testField1 := authFieldKey(fmt.Sprintf("test-field-1-%s", time.Now().Format(time.RFC3339)))
testValue1 := fmt.Sprintf("value-1-%s", time.Now().Format(time.RFC3339))
if !tt.noKey {
err := SetAuthField(testField1, testValue1)
if err != nil {
t.Fatalf("Failed to set \"%s\" as \"%s\": %v", testField1, testValue1, err)
}
}
err := DeleteAuthField(tt.key)
if err != nil {
t.Fatalf("Failed to delete field \"%s\": %v", tt.key, err)
}
// Check if key still exists
_, err = GetAuthField(tt.key)
if err == nil {
t.Fatalf("Key \"%s\" still exists after deletion", tt.key)
}
})
}
}
func TestDeleteAuthFieldWithProfile(t *testing.T) {
tests := []struct {
description string
keyringFails bool
profile string
key authFieldKey
noKey bool
}{
{
description: "base",
profile: "default",
key: "test-field-1",
},
{
description: "key doesnt exist",
profile: "default",
key: "doesnt-exist",
noKey: true,
},
{
description: "keyring fails",
profile: "default",
keyringFails: true,
key: "test-field-1",
},
{
description: "keyring fails, no key exists",
profile: "default",
keyringFails: true,
noKey: true,
},
{
description: "base, custom profile",
profile: "test-profile",
key: "test-field-1",
},
{
description: "key doesnt exist, custom profile",
profile: "test-profile",
key: "doesnt-exist",
noKey: true,
},
{
description: "keyring fails, custom profile",
profile: "test-profile",
keyringFails: true,
key: "test-field-1",
},
{
description: "keyring fails, no key exists, custom profile",
profile: "test-profile",
keyringFails: true,
noKey: true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
if !tt.keyringFails {
keyring.MockInit()
} else {
keyring.MockInitWithError(fmt.Errorf("keyring unavailable for testing"))
}
// Append random string to auth field key and value to avoid conflicts
testField1 := authFieldKey(fmt.Sprintf("test-field-1-%s", time.Now().Format(time.RFC3339)))
testValue1 := fmt.Sprintf("value-1-%s", time.Now().Format(time.RFC3339))
if !tt.noKey {
err := SetAuthField(testField1, testValue1)
if err != nil {
t.Fatalf("Failed to set \"%s\" as \"%s\": %v", testField1, testValue1, err)
}
}
err := deleteAuthFieldWithProfile(tt.profile, tt.key)
if err != nil {
t.Fatalf("Failed to delete field \"%s\": %v", tt.key, err)
}
// Check if key still exists
_, err = GetAuthField(tt.key)
if err == nil {
t.Fatalf("Key \"%s\" still exists after deletion", tt.key)
}
})
}
}
func TestDeleteAuthFieldKeyring(t *testing.T) {

@@ -611,3 +757,3 @@ tests := []struct {

err := DeleteProfileFromKeyring(tt.activeProfile)
err := DeleteProfileAuth(tt.activeProfile)
if err != nil {

@@ -773,3 +919,3 @@ if tt.isValid {

err = deleteAuthFieldProfile(tt.activeProfile)
err = deleteProfileFiles(tt.activeProfile)
if err != nil {

@@ -932,3 +1078,3 @@ t.Errorf("Post-test cleanup failed: remove profile \"%s\": %v. Please remove it manually", tt.activeProfile, err)

err = deleteAuthFieldProfile(tt.activeProfile)
err = deleteProfileFiles(tt.activeProfile)
if err != nil {

@@ -941,40 +1087,3 @@ t.Fatalf("Failed to remove profile: %v", err)

func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) error {
err := createEncodedTextFile(activeProfile)
if err != nil {
return err
}
textFileDir := config.GetProfileFolderPath(activeProfile)
textFilePath := filepath.Join(textFileDir, textFileName)
contentEncoded, err := os.ReadFile(textFilePath)
if err != nil {
return fmt.Errorf("read file: %w", err)
}
contentBytes, err := base64.StdEncoding.DecodeString(string(contentEncoded))
if err != nil {
return fmt.Errorf("decode file: %w", err)
}
content := map[authFieldKey]string{}
err = json.Unmarshal(contentBytes, &content)
if err != nil {
return fmt.Errorf("unmarshal file: %w", err)
}
delete(content, key)
contentBytes, err = json.Marshal(content)
if err != nil {
return fmt.Errorf("marshal file: %w", err)
}
contentEncoded = []byte(base64.StdEncoding.EncodeToString(contentBytes))
err = os.WriteFile(textFilePath, contentEncoded, 0o600)
if err != nil {
return fmt.Errorf("write file: %w", err)
}
return nil
}
func deleteAuthFieldProfile(activeProfile string) error {
func deleteProfileFiles(activeProfile string) error {
if activeProfile == config.DefaultProfileName {

@@ -999,1 +1108,117 @@ // Do not delete the default profile

}
func TestAuthorizeDeauthorizeUserProfileAuth(t *testing.T) {
type args struct {
sessionExpiresAtUnix string
accessToken string
refreshToken string
email string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "base",
args: args{
sessionExpiresAtUnix: "1234567890",
accessToken: "accessToken",
refreshToken: "refreshToken",
email: "test@example.com",
},
wantErr: false,
},
{
name: "no email",
args: args{
sessionExpiresAtUnix: "1234567890",
accessToken: "accessToken",
refreshToken: "refreshToken",
email: "",
},
wantErr: false,
},
{
name: "no session expires",
args: args{
sessionExpiresAtUnix: "",
accessToken: "accessToken",
refreshToken: "refreshToken",
email: "test@example.com",
},
wantErr: false,
},
{
name: "no access token",
args: args{
sessionExpiresAtUnix: "1234567890",
accessToken: "",
refreshToken: "refreshToken",
email: "test@example.com",
},
wantErr: false,
},
{
name: "no refresh token",
args: args{
sessionExpiresAtUnix: "1234567890",
accessToken: "accessToken",
refreshToken: "",
email: "test@example.com",
},
wantErr: false,
},
{
name: "all empty args",
args: args{
sessionExpiresAtUnix: "",
accessToken: "",
refreshToken: "",
email: "",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keyring.MockInit()
if err := LoginUser(tt.args.email, tt.args.accessToken, tt.args.refreshToken, tt.args.sessionExpiresAtUnix); (err != nil) != tt.wantErr {
t.Errorf("AuthorizeUserProfileAuth() error = %v, wantErr %v", err, tt.wantErr)
}
// Test values
testLoginAuthFields := []string{
tt.args.sessionExpiresAtUnix,
tt.args.accessToken,
tt.args.refreshToken,
tt.args.email,
}
// Check if the fields are set
for i := range loginAuthFieldKeys {
gotKey, err := GetAuthField(loginAuthFieldKeys[i])
if err != nil {
t.Errorf("Field \"%s\" not set after authorization", loginAuthFieldKeys[i])
}
expectedKey := testLoginAuthFields[i]
if gotKey != expectedKey {
t.Errorf("Field \"%s\" is wrong: expected \"%s\", got \"%s\"", loginAuthFieldKeys[i], expectedKey, gotKey)
}
}
if err := LogoutUser(); err != nil {
t.Errorf("DeauthorizeUserProfileAuth() error = %v", err)
}
// Check if the fields are deleted
for _, key := range loginAuthFieldKeys {
_, err := GetAuthField(key)
if err == nil {
t.Errorf("Field \"%s\" still exists after deauthorization", key)
}
}
})
}
}

@@ -65,2 +65,11 @@ package auth

// All fields that are set when a user logs in
// These fields should match the ones in LoginUser, which is ensured by the tests
var loginAuthFieldKeys = []authFieldKey{
SESSION_EXPIRES_AT_UNIX,
ACCESS_TOKEN,
REFRESH_TOKEN,
USER_EMAIL,
}
func SetAuthFlow(value AuthFlow) error {

@@ -109,2 +118,61 @@ return SetAuthField(authFlowType, string(value))

func DeleteAuthField(key authFieldKey) error {
activeProfile, err := config.GetProfile()
if err != nil {
return fmt.Errorf("get profile: %w", err)
}
return deleteAuthFieldWithProfile(activeProfile, key)
}
func deleteAuthFieldWithProfile(profile string, key authFieldKey) error {
err := deleteAuthFieldInKeyring(profile, key)
if err != nil {
// if the key is not found, we can ignore the error
if !errors.Is(err, keyring.ErrNotFound) {
errFallback := deleteAuthFieldInEncodedTextFile(profile, key)
if errFallback != nil {
return fmt.Errorf("delete from keyring failed (%w), try deleting from encoded text file: %w", err, errFallback)
}
}
}
return nil
}
func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) error {
err := createEncodedTextFile(activeProfile)
if err != nil {
return err
}
textFileDir := config.GetProfileFolderPath(activeProfile)
textFilePath := filepath.Join(textFileDir, textFileName)
contentEncoded, err := os.ReadFile(textFilePath)
if err != nil {
return fmt.Errorf("read file: %w", err)
}
contentBytes, err := base64.StdEncoding.DecodeString(string(contentEncoded))
if err != nil {
return fmt.Errorf("decode file: %w", err)
}
content := map[authFieldKey]string{}
err = json.Unmarshal(contentBytes, &content)
if err != nil {
return fmt.Errorf("unmarshal file: %w", err)
}
delete(content, key)
contentBytes, err = json.Marshal(content)
if err != nil {
return fmt.Errorf("marshal file: %w", err)
}
contentEncoded = []byte(base64.StdEncoding.EncodeToString(contentBytes))
err = os.WriteFile(textFilePath, contentEncoded, 0o600)
if err != nil {
return fmt.Errorf("write file: %w", err)
}
return nil
}
func deleteAuthFieldInKeyring(activeProfile string, key authFieldKey) error {

@@ -278,3 +346,28 @@ keyringServiceLocal := keyringService

func DeleteProfileFromKeyring(profile string) error {
func LoginUser(email, accessToken, refreshToken, sessionExpiresAtUnix string) error {
authFields := map[authFieldKey]string{
SESSION_EXPIRES_AT_UNIX: sessionExpiresAtUnix,
ACCESS_TOKEN: accessToken,
REFRESH_TOKEN: refreshToken,
USER_EMAIL: email,
}
err := SetAuthFieldMap(authFields)
if err != nil {
return fmt.Errorf("set auth fields: %w", err)
}
return nil
}
func LogoutUser() error {
for _, key := range loginAuthFieldKeys {
err := DeleteAuthField(key)
if err != nil {
return fmt.Errorf("delete auth field \"%s\": %w", key, err)
}
}
return nil
}
func DeleteProfileAuth(profile string) error {
err := config.ValidateProfile(profile)

@@ -290,8 +383,5 @@ if err != nil {

for _, key := range authFieldKeys {
err := deleteAuthFieldInKeyring(profile, key)
err := deleteAuthFieldWithProfile(profile, key)
if err != nil {
// if the key is not found, we can ignore the error
if !errors.Is(err, keyring.ErrNotFound) {
return fmt.Errorf("delete auth field \"%s\" from keyring: %w", key, err)
}
return fmt.Errorf("delete auth field \"%s\": %w", key, err)
}

@@ -298,0 +388,0 @@ }

@@ -170,9 +170,3 @@ package auth

authFields := map[authFieldKey]string{
SESSION_EXPIRES_AT_UNIX: sessionExpiresAtUnix,
ACCESS_TOKEN: accessToken,
REFRESH_TOKEN: refreshToken,
USER_EMAIL: email,
}
err = SetAuthFieldMap(authFields)
err = LoginUser(email, accessToken, refreshToken, sessionExpiresAtUnix)
if err != nil {

@@ -179,0 +173,0 @@ errServer = fmt.Errorf("set in auth storage: %w", err)