From 825d6df8e3cfc9622ae1bbff49e4878dfd39c2e1 Mon Sep 17 00:00:00 2001 From: Rustam <16064414+rusq@users.noreply.github.com> Date: Fri, 17 Jan 2025 22:35:49 +1300 Subject: [PATCH 1/5] custom machine ID --- go.mod | 8 +-- go.sum | 8 +++ internal/cache/auth.go | 55 +++++++++++------ internal/cache/auth_test.go | 116 ++++++++++++++++++++++++++++++++---- internal/cache/manager.go | 28 +++++++-- 5 files changed, 179 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index add9e621..63ad437e 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/playwright-community/playwright-go v0.4901.0 github.com/rusq/chttp v1.0.2 - github.com/rusq/encio v0.1.0 + github.com/rusq/encio v0.2.0 github.com/rusq/fsadapter v1.0.2 github.com/rusq/osenv/v2 v2.0.1 github.com/rusq/rbubbles v0.0.2 @@ -39,7 +39,7 @@ require ( github.com/yuin/goldmark-emoji v1.0.4 go.uber.org/mock v0.5.0 golang.org/x/sync v0.10.0 - golang.org/x/term v0.27.0 + golang.org/x/term v0.28.0 golang.org/x/text v0.21.0 golang.org/x/time v0.8.0 ) @@ -87,8 +87,8 @@ require ( github.com/ysmood/got v0.40.0 // indirect github.com/ysmood/gson v0.7.3 // indirect github.com/ysmood/leakless v0.9.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.32.0 // indirect golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0aeb6e12..73b1cb90 100644 --- a/go.sum +++ b/go.sum @@ -149,6 +149,8 @@ github.com/rusq/chttp v1.0.2 h1:bc8FTKE/l318Kie3sb2KrGi7Fu5tSDQY+JiXMsq4fO8= github.com/rusq/chttp v1.0.2/go.mod h1:bmuoQMUFs9fmigUmT7xbp8s0rHyzUrf7+78yLklr1so= github.com/rusq/encio v0.1.0 h1:DauNaVtIf79kILExhMGIsE5svYwPnDSksdYP0oVVcr8= github.com/rusq/encio v0.1.0/go.mod h1:AP3lDpo/BkcHcOMNduBlZdd0sbwhruq6+NZtYm5Mxb0= +github.com/rusq/encio v0.2.0 h1:+EbYnoLrX/mfwjBp0HqozdfOB2EplNDgbA2vIQvnCuY= +github.com/rusq/encio v0.2.0/go.mod h1:AP3lDpo/BkcHcOMNduBlZdd0sbwhruq6+NZtYm5Mxb0= github.com/rusq/fsadapter v1.0.2 h1:T+hG8nvA4WlM5oLRcUMEPEOdmDWEd9wmD2oNpztwz90= github.com/rusq/fsadapter v1.0.2/go.mod h1:oqHuzWZKhum7JBLCUi/mkyrxngtfemuJB0HgEJ6595A= github.com/rusq/osenv/v2 v2.0.1 h1:1LtNt8VNV/W86wb38Hyu5W3Rwqt/F1JNRGE+8GRu09o= @@ -199,6 +201,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -227,6 +231,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -234,6 +240,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/internal/cache/auth.go b/internal/cache/auth.go index 10cebff7..b1ad30a5 100644 --- a/internal/cache/auth.go +++ b/internal/cache/auth.go @@ -120,14 +120,23 @@ func ezLoginTested() bool { } } -// filer is openCreator that will be used by InitProvider. -var filer container = encryptedFile{} - type Credentials interface { IsEmpty() bool AuthProvider(ctx context.Context, workspace string, opts ...auth.Option) (auth.Provider, error) } +type authenticator struct { + container container + cacheDir string +} + +func newAuthenticator(cacheDir, machineID string) authenticator { + return authenticator{ + cacheDir: cacheDir, + container: encryptedFile{machineID: machineID}, + } +} + // initProvider initialises the auth.Provider depending on provided slack // credentials. It returns auth.Provider or an error. The logic diagram is // available in the doc/diagrams/auth_flow.puml. @@ -142,22 +151,22 @@ type Credentials interface { // stored credentials on another machine (including virtual), even another // operating system on the same machine, unless it's a clone of the source // operating system on which the credentials storage was created. -func initProvider(ctx context.Context, cacheDir string, filename string, workspace string, creds Credentials, opts ...auth.Option) (auth.Provider, error) { +func (a authenticator) initProvider(ctx context.Context, filename string, workspace string, creds Credentials, opts ...auth.Option) (auth.Provider, error) { ctx, task := trace.NewTask(ctx, "initProvider") defer task.End() credsFile := filename - if cacheDir != "" { - if err := os.MkdirAll(cacheDir, 0o700); err != nil { + if a.cacheDir != "" { + if err := os.MkdirAll(a.cacheDir, 0o700); err != nil { return nil, fmt.Errorf("failed to create cache directory: %w", err) } - credsFile = filepath.Join(cacheDir, filename) + credsFile = filepath.Join(a.cacheDir, filename) } // try to load the existing credentials, if saved earlier. - lg := slog.With("cache_dir", cacheDir, "filename", filename, "workspace", workspace) + lg := slog.With("cache_dir", a.cacheDir, "filename", filename, "workspace", workspace) if creds == nil || creds.IsEmpty() { - if prov, err := tryLoad(ctx, credsFile); err != nil { + if prov, err := a.tryLoad(ctx, credsFile); err != nil { msg := fmt.Sprintf("failed to load saved credentials: %s", err) trace.Log(ctx, "warn", msg) slog.DebugContext(ctx, msg) @@ -179,7 +188,7 @@ func initProvider(ctx context.Context, cacheDir string, filename string, workspa return nil, fmt.Errorf("failed to initialise the auth provider: %w", err) } - if err := saveCreds(filer, credsFile, provider); err != nil { + if err := saveCreds(a.container, credsFile, provider); err != nil { trace.Logf(ctx, "error", "failed to save credentials to: %s", credsFile) } @@ -188,8 +197,8 @@ func initProvider(ctx context.Context, cacheDir string, filename string, workspa var authTester = (auth.Provider).Test -func tryLoad(ctx context.Context, filename string) (auth.Provider, error) { - prov, err := loadCreds(filer, filename) +func (a authenticator) tryLoad(ctx context.Context, filename string) (auth.Provider, error) { + prov, err := loadCreds(a.container, filename) if err != nil { return nil, err } @@ -234,14 +243,26 @@ type container interface { } // encryptedFile is the encrypted file container. -type encryptedFile struct{} +type encryptedFile struct { + // machineID is the machine ID override. If it is empty, the actual machine + // ID is used. + machineID string +} -func (encryptedFile) Open(filename string) (io.ReadCloser, error) { - return encio.Open(filename) +func (f encryptedFile) Open(filename string) (io.ReadCloser, error) { + var opts []encio.Option + if f.machineID != "" { + opts = append(opts, encio.WithID(f.machineID)) + } + return encio.Open(filename, opts...) } -func (encryptedFile) Create(filename string) (io.WriteCloser, error) { - return encio.Create(filename) +func (f encryptedFile) Create(filename string) (io.WriteCloser, error) { + var opts []encio.Option + if f.machineID != "" { + opts = append(opts, encio.WithID(f.machineID)) + } + return encio.Create(filename, opts...) } // EZLoginFlags is a diagnostic function that returns the map of flags that diff --git a/internal/cache/auth_test.go b/internal/cache/auth_test.go index be81a0d5..eb0ca141 100644 --- a/internal/cache/auth_test.go +++ b/internal/cache/auth_test.go @@ -4,15 +4,16 @@ import ( "bytes" "context" "errors" + "io" "os" "path/filepath" "reflect" "testing" + "github.com/rusq/slack" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" - "github.com/rusq/slack" "github.com/rusq/slackdump/v3/auth" "github.com/rusq/slackdump/v3/internal/fixtures" "github.com/rusq/slackdump/v3/internal/mocks/mock_appauth" @@ -21,7 +22,7 @@ import ( func Test_isExistingFile(t *testing.T) { testfile := filepath.Join(t.TempDir(), "cookies.txt") - if err := os.WriteFile(testfile, []byte("blah"), 0600); err != nil { + if err := os.WriteFile(testfile, []byte("blah"), 0o600); err != nil { t.Fatal(err) } @@ -48,7 +49,7 @@ func Test_isExistingFile(t *testing.T) { func TestAuthData_Type(t *testing.T) { dir := t.TempDir() testFile := filepath.Join(dir, "fake_cookie") - if err := os.WriteFile(testFile, []byte("unittest"), 0644); err != nil { + if err := os.WriteFile(testFile, []byte("unittest"), 0o644); err != nil { t.Fatal(err) } type fields struct { @@ -151,7 +152,7 @@ func TestInitProvider(t *testing.T) { AuthProvider(gomock.Any(), "wsp"). Return(storedProv, nil) }, - nil, //not used in the test + nil, // not used in the test storedProv, false, }, @@ -221,15 +222,17 @@ func TestInitProvider(t *testing.T) { // resetting credentials credsFile := filepath.Join(testDir, defCredsFile) - if err := saveCreds(filer, credsFile, storedProv); err != nil { + container := encryptedFile{} + if err := saveCreds(container, credsFile, storedProv); err != nil { t.Fatal(err) } mc := mock_appauth.NewMockCredentials(gomock.NewController(t)) tt.expect(mc) + auther := newAuthenticator(tt.args.cacheDir, "") // test - got, err := initProvider(tt.args.ctx, tt.args.cacheDir, defCredsFile, tt.args.workspace, mc) + got, err := auther.initProvider(tt.args.ctx, defCredsFile, tt.args.workspace, mc) if (err != nil) != tt.wantErr { t.Errorf("InitProvider() error = %v, wantErr %v", err, tt.wantErr) return @@ -252,6 +255,8 @@ func Test_tryLoad(t *testing.T) { testDir := t.TempDir() testProvider, _ := auth.NewValueAuth("xoxc", "xoxd") credsFile := filepath.Join(testDir, defCredsFile) + + filer := encryptedFile{} if err := saveCreds(filer, credsFile, testProvider); err != nil { t.Fatal(err) } @@ -298,7 +303,11 @@ func Test_tryLoad(t *testing.T) { }() authTester = fakeAuthTester(tt.authTestErr) - got, err := tryLoad(tt.args.ctx, tt.args.filename) + a := authenticator{ + container: encryptedFile{}, + } + + got, err := a.tryLoad(tt.args.ctx, tt.args.filename) if (err != nil) != tt.wantErr { t.Errorf("tryLoad() error = %v, wantErr %v", err, tt.wantErr) return @@ -316,7 +325,7 @@ func Test_loadCreds(t *testing.T) { if err := auth.Save(&buf, testProv); err != nil { t.Fatal(err) } - var testProvBytes = buf.Bytes() + testProvBytes := buf.Bytes() type args struct { filename string @@ -400,7 +409,7 @@ func Test_saveCreds(t *testing.T) { if err := auth.Save(&buf, testProv); err != nil { t.Fatal(err) } - var testProvBytes = buf.Bytes() + testProvBytes := buf.Bytes() type args struct { filename string @@ -461,7 +470,7 @@ func TestAuthReset(t *testing.T) { t.Run("file is removed", func(t *testing.T) { tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, defCredsFile) - if err := os.WriteFile(testFile, []byte("unit"), 0644); err != nil { + if err := os.WriteFile(testFile, []byte("unit"), 0o644); err != nil { t.Fatal(err) } if err := AuthReset(tmpDir); err != nil { @@ -472,3 +481,90 @@ func TestAuthReset(t *testing.T) { } }) } + +func Test_encryptedFile_Open(t *testing.T) { + tmpdir := t.TempDir() + mkfile := func(machineID string, contents []byte) string { + c := encryptedFile{machineID: machineID} + tf, err := os.CreateTemp(tmpdir, "") + if err != nil { + panic(err) + } + tf.Close() + + ef, err := c.Create(tf.Name()) + if err != nil { + panic(err) + } + defer ef.Close() + if _, err := io.Copy(ef, bytes.NewReader(contents)); err != nil { + panic(err) + } + return tf.Name() + } + + type fields struct { + machineID string + } + type args struct { + filename string + } + tests := []struct { + name string + fields fields + contents []byte + args args + wantMatch bool + wantErr bool + }{ + { + "encrypted with the same machine ID", + fields{ + machineID: "123", + }, + []byte("unit test"), + args{mkfile("123", []byte("unit test"))}, + true, + false, + }, + { + "different machine ID", + fields{ + machineID: "123", + }, + []byte("unit test"), + args{mkfile("456", []byte("unit test"))}, + false, + false, + }, + { + "override vs real ID", + fields{}, + []byte("unit test"), + args{mkfile("456", []byte("unit test"))}, + false, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := encryptedFile{ + machineID: tt.fields.machineID, + } + got, err := f.Open(tt.args.filename) + if (err != nil) != tt.wantErr { + t.Errorf("encryptedFile.Open() error = %v, wantErr %v", err, tt.wantErr) + return + } + defer got.Close() + var contents bytes.Buffer + if _, err := io.Copy(&contents, got); err != nil { + t.Fatalf("failed to read the test file: %s", err) + } + + if bytes.Equal(contents.Bytes(), tt.contents) != tt.wantMatch { + t.Errorf("encryptedFile.Open() = %#v, want %#v", contents.Bytes(), tt.contents) + } + }) + } +} diff --git a/internal/cache/manager.go b/internal/cache/manager.go index 4a179d47..86611155 100644 --- a/internal/cache/manager.go +++ b/internal/cache/manager.go @@ -40,6 +40,8 @@ type Manager struct { userFile string channelFile string + // machineID is the machine ID override for encryption/decryption. + machineID string } const ( @@ -65,6 +67,15 @@ func WithAuthOpts(opts ...auth.Option) Option { } } +// WithMachineID allows to set the machine ID for encryption/decryption. +func WithMachineID(id string) Option { + return func(m *Manager) { + if id != "" { + m.machineID = id + } + } +} + // WithChannelCacheBase allows to change the default cache file name for // channels cache. func WithChannelCacheBase(filename string) Option { @@ -113,7 +124,7 @@ func NewManager(dir string, opts ...Option) (*Manager, error) { opt(m) } if m.dir != "" { - if err := os.MkdirAll(dir, 0700); err != nil { + if err := os.MkdirAll(dir, 0o700); err != nil { return nil, err } } @@ -136,16 +147,19 @@ func NewManager(dir string, opts ...Option) (*Manager, error) { // operating system on the same machine, unless it's a clone of the source // operating system on which the credentials storage was created. func (m *Manager) Auth(ctx context.Context, name string, c Credentials) (auth.Provider, error) { - return initProvider(ctx, m.dir, m.filename(name), name, c, m.authOptions...) + a := newAuthenticator(m.dir, m.machineID) + return a.initProvider(ctx, m.filename(name), name, c, m.authOptions...) } // LoadProvider loads the file from disk without any checks. func (m *Manager) LoadProvider(name string) (auth.Provider, error) { + filer := encryptedFile{machineID: m.machineID} return loadCreds(filer, m.filepath(name)) } // saveProvider saves the provider to the file, no questions asked. func (m *Manager) saveProvider(name string, p auth.Provider) error { + filer := encryptedFile{machineID: m.machineID} return saveCreds(filer, m.filepath(name), p) } @@ -189,7 +203,7 @@ func (m *Manager) List() ([]string, error) { if err != nil { return nil, err } - var workspaces = make([]string, len(files)) + workspaces := make([]string, len(files)) for i := range files { name, err := m.name(files[i]) if err != nil { @@ -239,7 +253,7 @@ func (m *Manager) Current() (string, error) { // selectDefault selects the default workspace if it exists. func (m *Manager) selectDefault() (string, error) { - var wsp = defName + wsp := defName if !m.HasDefault() { // the default workspace does not exist, pick any w, err := m.firstAvailable() @@ -369,6 +383,10 @@ func wspName(filename string) string { // WalkUsers scans the cache directory and calls userFn for each user file // discovered. func (m *Manager) WalkUsers(userFn func(path string, r io.Reader) error) error { + var encopts []encio.Option + if m.machineID != "" { + encopts = append(encopts, encio.WithID(m.machineID)) + } userSuffix := filepath.Ext(m.userFile) userPrefix := m.userFile[0 : len(m.userFile)-len(userSuffix)] err := filepath.WalkDir(m.dir, func(path string, d fs.DirEntry, err error) error { @@ -379,7 +397,7 @@ func (m *Manager) WalkUsers(userFn func(path string, r io.Reader) error) error { // skip non-matching files return nil } - f, err := encio.Open(path) + f, err := encio.Open(path, encopts...) if err != nil { return err } From f54768ebb49153677771825b0f4d11b82965e584 Mon Sep 17 00:00:00 2001 From: Rustam <16064414+rusq@users.noreply.github.com> Date: Fri, 17 Jan 2025 22:51:27 +1300 Subject: [PATCH 2/5] Update tests --- internal/cache/auth.go | 37 ++++++++++++++++++++----------------- internal/cache/auth_test.go | 10 +++++++++- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/internal/cache/auth.go b/internal/cache/auth.go index b1ad30a5..134624ca 100644 --- a/internal/cache/auth.go +++ b/internal/cache/auth.go @@ -59,7 +59,7 @@ func (c AuthData) Type(context.Context) (AuthType, error) { ez = ATPlaywright } if !c.IsEmpty() { - if isExistingFile(c.Cookie) { + if exists(c.Cookie) { return ATCookieFile, nil } return ATValue, nil @@ -102,7 +102,7 @@ func (c AuthData) AuthProvider(ctx context.Context, workspace string, opts ...au return nil, errors.New("internal error: unsupported auth type") } -func isExistingFile(name string) bool { +func exists(name string) bool { fi, err := os.Stat(name) return err == nil && !fi.IsDir() } @@ -126,14 +126,14 @@ type Credentials interface { } type authenticator struct { - container container - cacheDir string + ct createOpener + dir string } func newAuthenticator(cacheDir, machineID string) authenticator { return authenticator{ - cacheDir: cacheDir, - container: encryptedFile{machineID: machineID}, + dir: cacheDir, + ct: encryptedFile{machineID: machineID}, } } @@ -150,21 +150,22 @@ func newAuthenticator(cacheDir, machineID string) authenticator { // the operating system (see package encio), it makes it impossible use the // stored credentials on another machine (including virtual), even another // operating system on the same machine, unless it's a clone of the source -// operating system on which the credentials storage was created. +// operating system on which the credentials storage was created. Optionally it +// can be overridden by providing a machine ID override to [newAuthenticator]. func (a authenticator) initProvider(ctx context.Context, filename string, workspace string, creds Credentials, opts ...auth.Option) (auth.Provider, error) { ctx, task := trace.NewTask(ctx, "initProvider") defer task.End() credsFile := filename - if a.cacheDir != "" { - if err := os.MkdirAll(a.cacheDir, 0o700); err != nil { + if a.dir != "" { + if err := os.MkdirAll(a.dir, 0o700); err != nil { return nil, fmt.Errorf("failed to create cache directory: %w", err) } - credsFile = filepath.Join(a.cacheDir, filename) + credsFile = filepath.Join(a.dir, filename) } // try to load the existing credentials, if saved earlier. - lg := slog.With("cache_dir", a.cacheDir, "filename", filename, "workspace", workspace) + lg := slog.With("cache_dir", a.dir, "filename", filename, "workspace", workspace) if creds == nil || creds.IsEmpty() { if prov, err := a.tryLoad(ctx, credsFile); err != nil { msg := fmt.Sprintf("failed to load saved credentials: %s", err) @@ -188,7 +189,7 @@ func (a authenticator) initProvider(ctx context.Context, filename string, worksp return nil, fmt.Errorf("failed to initialise the auth provider: %w", err) } - if err := saveCreds(a.container, credsFile, provider); err != nil { + if err := saveCreds(a.ct, credsFile, provider); err != nil { trace.Logf(ctx, "error", "failed to save credentials to: %s", credsFile) } @@ -198,7 +199,7 @@ func (a authenticator) initProvider(ctx context.Context, filename string, worksp var authTester = (auth.Provider).Test func (a authenticator) tryLoad(ctx context.Context, filename string) (auth.Provider, error) { - prov, err := loadCreds(a.container, filename) + prov, err := loadCreds(a.ct, filename) if err != nil { return nil, err } @@ -210,7 +211,7 @@ func (a authenticator) tryLoad(ctx context.Context, filename string) (auth.Provi } // loadCreds loads the encrypted credentials from the file. -func loadCreds(ct container, filename string) (auth.Provider, error) { +func loadCreds(ct createOpener, filename string) (auth.Provider, error) { f, err := ct.Open(filename) if err != nil { return nil, errors.New("failed to load stored credentials") @@ -221,7 +222,7 @@ func loadCreds(ct container, filename string) (auth.Provider, error) { } // saveCreds encrypts and saves the credentials. -func saveCreds(ct container, filename string, p auth.Provider) error { +func saveCreds(ct createOpener, filename string, p auth.Provider) error { f, err := ct.Create(filename) if err != nil { return err @@ -236,12 +237,14 @@ func AuthReset(cacheDir string) error { return os.Remove(filepath.Join(cacheDir, defCredsFile)) } -// container is the interface to operate with credentials container. -type container interface { +// createOpener is the interface to operate with credentials createOpener. +type createOpener interface { Create(filename string) (io.WriteCloser, error) Open(filename string) (io.ReadCloser, error) } +var _ createOpener = encryptedFile{} + // encryptedFile is the encrypted file container. type encryptedFile struct { // machineID is the machine ID override. If it is empty, the actual machine diff --git a/internal/cache/auth_test.go b/internal/cache/auth_test.go index eb0ca141..1d3cd29b 100644 --- a/internal/cache/auth_test.go +++ b/internal/cache/auth_test.go @@ -304,7 +304,7 @@ func Test_tryLoad(t *testing.T) { authTester = fakeAuthTester(tt.authTestErr) a := authenticator{ - container: encryptedFile{}, + ct: encryptedFile{}, } got, err := a.tryLoad(tt.args.ctx, tt.args.filename) @@ -545,6 +545,14 @@ func Test_encryptedFile_Open(t *testing.T) { false, false, }, + { + "machine ID", + fields{}, + []byte("unit test"), + args{mkfile("", []byte("unit test"))}, + true, + false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 62c5493e13c365b4a50c76a67704ca9cb93340c0 Mon Sep 17 00:00:00 2001 From: Rustam <16064414+rusq@users.noreply.github.com> Date: Sat, 18 Jan 2025 12:07:29 +1300 Subject: [PATCH 3/5] Configuration entry for machine ID --- cmd/slackdump/internal/cfg/cfg.go | 8 +++++++- internal/cache/auth_test.go | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/slackdump/internal/cfg/cfg.go b/cmd/slackdump/internal/cfg/cfg.go index 7a3f2a5f..3d802043 100644 --- a/cmd/slackdump/internal/cfg/cfg.go +++ b/cmd/slackdump/internal/cfg/cfg.go @@ -39,6 +39,7 @@ var ( Browser browser.Browser LegacyBrowser bool ForceEnterprise bool + MachineID string // Machine ID override MemberOnly bool DownloadFiles bool @@ -90,6 +91,7 @@ const ( OmitChunkCacheFlag OmitMemberOnlyFlag OmitRecordFilesFlag + OmitMachineIDOvrFlag OmitAll = OmitConfigFlag | OmitDownloadFlag | @@ -101,7 +103,8 @@ const ( OmitTimeframeFlag | OmitChunkCacheFlag | OmitMemberOnlyFlag | - OmitRecordFilesFlag + OmitRecordFilesFlag | + OmitMachineIDOvrFlag ) // SetBaseFlags sets base flags @@ -122,6 +125,9 @@ func SetBaseFlags(fs *flag.FlagSet, mask FlagMask) { fs.StringVar(&RODUserAgent, "user-agent", "", "override the user agent string for EZ-Login 3000") fs.BoolVar(&LoadSecrets, "load-env", false, "load secrets from the .env, .env.txt or secrets.txt file") } + if mask&OmitMachineIDOvrFlag == 0 { + fs.StringVar(&MachineID, "machine-id", osenv.Secret("MACHINE_ID", ""), "override the machine ID for encryption") + } if mask&OmitDownloadFlag == 0 { fs.BoolVar(&DownloadFiles, "files", true, "enables file attachments download (to disable, specify: -files=false)") } diff --git a/internal/cache/auth_test.go b/internal/cache/auth_test.go index 1d3cd29b..957cea81 100644 --- a/internal/cache/auth_test.go +++ b/internal/cache/auth_test.go @@ -39,7 +39,7 @@ func Test_isExistingFile(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := isExistingFile(tt.args.cookie); got != tt.want { + if got := exists.cookie); got != tt.want { t.Errorf("isExistingFile() = %v, want %v", got, tt.want) } }) From d9f9e4db12b34a34803d0ae8c814b9cc02d476df Mon Sep 17 00:00:00 2001 From: Rustam Gilyazov <16064414+rusq@users.noreply.github.com> Date: Sat, 18 Jan 2025 15:54:00 +1000 Subject: [PATCH 4/5] Machine ID Override --- cmd/slackdump/internal/cfg/cfg.go | 11 +- cmd/slackdump/internal/diag/hydrate_test.go | 11 +- cmd/slackdump/internal/diag/info/auth.go | 7 +- cmd/slackdump/internal/diag/info/workspace.go | 6 +- cmd/slackdump/internal/diag/obfuscate.go | 9 +- cmd/slackdump/internal/diag/uninstall.go | 5 +- cmd/slackdump/internal/format/format.go | 3 +- cmd/slackdump/internal/list/common.go | 3 +- cmd/slackdump/internal/workspace/del.go | 2 +- cmd/slackdump/internal/workspace/import.go | 3 +- cmd/slackdump/internal/workspace/list.go | 4 +- cmd/slackdump/internal/workspace/new.go | 5 +- cmd/slackdump/internal/workspace/select.go | 2 +- .../internal/workspace/wiz_select.go | 6 +- cmd/slackdump/internal/workspace/workspace.go | 9 +- .../workspace/workspaceui/workspaceui.go | 6 +- internal/cache/auth.go | 13 +- internal/cache/auth_test.go | 44 +++---- internal/cache/manager.go | 2 +- internal/cache/manager_test.go | 50 ++++++- internal/fixtures/export.go | 1 - internal/mocks/mock_appauth/mock_appauth.go | 124 ------------------ .../mocks/mock_cache/mock_cache.go | 36 ++--- internal/mocks/mock_dl/mock_exporter.go | 80 ----------- internal/mocks/mock_fsadapter/mock_fs.go | 65 --------- 25 files changed, 148 insertions(+), 359 deletions(-) delete mode 100644 internal/mocks/mock_appauth/mock_appauth.go rename mocks/mock_appauth/mock_appauth.go => internal/mocks/mock_cache/mock_cache.go (74%) delete mode 100644 internal/mocks/mock_dl/mock_exporter.go delete mode 100644 internal/mocks/mock_fsadapter/mock_fs.go diff --git a/cmd/slackdump/internal/cfg/cfg.go b/cmd/slackdump/internal/cfg/cfg.go index 3d802043..ecdb4c83 100644 --- a/cmd/slackdump/internal/cfg/cfg.go +++ b/cmd/slackdump/internal/cfg/cfg.go @@ -39,7 +39,7 @@ var ( Browser browser.Browser LegacyBrowser bool ForceEnterprise bool - MachineID string // Machine ID override + MachineIDOvr string // Machine ID override MemberOnly bool DownloadFiles bool @@ -91,7 +91,6 @@ const ( OmitChunkCacheFlag OmitMemberOnlyFlag OmitRecordFilesFlag - OmitMachineIDOvrFlag OmitAll = OmitConfigFlag | OmitDownloadFlag | @@ -103,8 +102,7 @@ const ( OmitTimeframeFlag | OmitChunkCacheFlag | OmitMemberOnlyFlag | - OmitRecordFilesFlag | - OmitMachineIDOvrFlag + OmitRecordFilesFlag ) // SetBaseFlags sets base flags @@ -125,8 +123,9 @@ func SetBaseFlags(fs *flag.FlagSet, mask FlagMask) { fs.StringVar(&RODUserAgent, "user-agent", "", "override the user agent string for EZ-Login 3000") fs.BoolVar(&LoadSecrets, "load-env", false, "load secrets from the .env, .env.txt or secrets.txt file") } - if mask&OmitMachineIDOvrFlag == 0 { - fs.StringVar(&MachineID, "machine-id", osenv.Secret("MACHINE_ID", ""), "override the machine ID for encryption") + if mask&OmitAuthFlags == 0 || mask&OmitCacheDir == 0 { + // machine-id flag will be automatically enabled if auth flags or cache dir flags are enabled. + fs.StringVar(&MachineIDOvr, "machine-id", osenv.Secret("MACHINE_ID_OVERRIDE", ""), "override the machine ID for encryption") } if mask&OmitDownloadFlag == 0 { fs.BoolVar(&DownloadFiles, "files", true, "enables file attachments download (to disable, specify: -files=false)") diff --git a/cmd/slackdump/internal/diag/hydrate_test.go b/cmd/slackdump/internal/diag/hydrate_test.go index 7fdecf72..8118e208 100644 --- a/cmd/slackdump/internal/diag/hydrate_test.go +++ b/cmd/slackdump/internal/diag/hydrate_test.go @@ -13,7 +13,8 @@ import ( "github.com/rusq/slack" gomock "go.uber.org/mock/gomock" - "github.com/rusq/slackdump/v3/internal/mocks/mock_fsadapter" + "github.com/rusq/fsadapter/mocks/mock_fsadapter" + "github.com/rusq/slackdump/v3/mocks/mock_downloader" ) @@ -148,12 +149,12 @@ func (f *fakewritecloser) Close() error { func Test_downloadFiles(t *testing.T) { tests := []struct { name string - expectFn func(m *Mocksourcer, fs *mock_fsadapter.MockFS, d *mock_downloader.MockGetFiler) + expectFn func(m *Mocksourcer, fs *mock_fsadapter.MockFSCloser, d *mock_downloader.MockGetFiler) wantErr bool }{ { "single message w 2 files", - func(m *Mocksourcer, fs *mock_fsadapter.MockFS, d *mock_downloader.MockGetFiler) { + func(m *Mocksourcer, fs *mock_fsadapter.MockFSCloser, d *mock_downloader.MockGetFiler) { m.EXPECT().Channels().Return(TestChannels, nil) m.EXPECT().AllMessages("C01").Return([]slack.Message{TestMsgWFile1}, nil) @@ -167,7 +168,7 @@ func Test_downloadFiles(t *testing.T) { }, { "all ok", - func(m *Mocksourcer, fs *mock_fsadapter.MockFS, d *mock_downloader.MockGetFiler) { + func(m *Mocksourcer, fs *mock_fsadapter.MockFSCloser, d *mock_downloader.MockGetFiler) { m.EXPECT().Channels().Return(TestChannels, nil) m.EXPECT().AllMessages("C01").Return(TestMessages, nil) m.EXPECT().AllThreadMessages("C01", "2").Return(TestThreadMessages, nil) @@ -189,7 +190,7 @@ func Test_downloadFiles(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) ms := NewMocksourcer(ctrl) - fs := mock_fsadapter.NewMockFS(ctrl) + fs := mock_fsadapter.NewMockFSCloser(ctrl) d := mock_downloader.NewMockGetFiler(ctrl) if tt.expectFn != nil { tt.expectFn(ms, fs, d) diff --git a/cmd/slackdump/internal/diag/info/auth.go b/cmd/slackdump/internal/diag/info/auth.go index 7e03092b..c38ab5ce 100644 --- a/cmd/slackdump/internal/diag/info/auth.go +++ b/cmd/slackdump/internal/diag/info/auth.go @@ -13,16 +13,15 @@ import ( "github.com/rusq/slackdump/v3/auth" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" - "github.com/rusq/slackdump/v3/internal/cache" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace" ) func CollectAuth(ctx context.Context, w io.Writer) error { - // lg := logger.FromContext(ctx) fmt.Fprintln(os.Stderr, "To confirm the operation, please enter your OS password.") if err := osValidateUser(ctx, os.Stderr); err != nil { return err } - m, err := cache.NewManager(cfg.CacheDir()) + m, err := workspace.CacheMgr() if err != nil { return fmt.Errorf("cache error: %w", err) } @@ -34,7 +33,7 @@ func CollectAuth(ctx context.Context, w io.Writer) error { if err != nil { return fmt.Errorf("cache error: %w", err) } - f, err := encio.Open(filepath.Join(cfg.CacheDir(), fi.Name())) + f, err := encio.Open(filepath.Join(cfg.CacheDir(), fi.Name()), encio.WithID(cfg.MachineIDOvr)) if err != nil { return fmt.Errorf("cache error: %w", err) } diff --git a/cmd/slackdump/internal/diag/info/workspace.go b/cmd/slackdump/internal/diag/info/workspace.go index 323748ff..bfc7bd30 100644 --- a/cmd/slackdump/internal/diag/info/workspace.go +++ b/cmd/slackdump/internal/diag/info/workspace.go @@ -2,7 +2,7 @@ package info import ( "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" - "github.com/rusq/slackdump/v3/internal/cache" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace" ) type Workspace struct { @@ -13,10 +13,10 @@ type Workspace struct { } func (inf *Workspace) collect(replaceFn PathReplFunc) { - inf.Path = replaceFn(cfg.LocalCacheDir) + inf.Path = replaceFn(cfg.CacheDir()) inf.Count = -1 // Workspace information - m, err := cache.NewManager(cfg.LocalCacheDir) + m, err := workspace.CacheMgr() if err != nil { inf.Path = loser(err) return diff --git a/cmd/slackdump/internal/diag/obfuscate.go b/cmd/slackdump/internal/diag/obfuscate.go index 1176e3f1..c0e90559 100644 --- a/cmd/slackdump/internal/diag/obfuscate.go +++ b/cmd/slackdump/internal/diag/obfuscate.go @@ -86,11 +86,12 @@ func runObfuscate(ctx context.Context, cmd *base.Command, args []string) error { inType := objtype(obfparam.input) var fn func(context.Context) error - if inType == otFile || inType == otTerm { + switch inType { + case otFile, otTerm: fn = obfFile - } else if inType == otDir { + case otDir: fn = obfDir - } else { + default: base.SetExitStatus(base.SInvalidParameters) return fmt.Errorf("input %s is invalid", obfparam.input) } @@ -194,7 +195,7 @@ func obfDir(ctx context.Context) error { InName: obfparam.input, } case otNotExist: - if err := os.MkdirAll(obfparam.output, 0755); err != nil { + if err := os.MkdirAll(obfparam.output, 0o755); err != nil { return err } case otDir: diff --git a/cmd/slackdump/internal/diag/uninstall.go b/cmd/slackdump/internal/diag/uninstall.go index 94b18e7c..7f3823c2 100644 --- a/cmd/slackdump/internal/diag/uninstall.go +++ b/cmd/slackdump/internal/diag/uninstall.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/rusq/slackauth" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/diag/info" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" @@ -16,6 +17,7 @@ import ( "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui/cfgui" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui/dumpui" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui/updaters" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace" "github.com/rusq/slackdump/v3/internal/cache" ) @@ -84,7 +86,6 @@ func init() { cmdUninstall.Flag.BoolVar(&uninstParams.purge, "purge", false, "remove everything (same as -rod -playwright -cache)") cmdUninstall.Flag.BoolVar(&uninstParams.dry, "dry", false, "dry run") cmdUninstall.Flag.BoolVar(&uninstParams.noConfirm, "no-confirm", false, "no confirmation from the user") - } func runUninstall(ctx context.Context, cmd *base.Command, args []string) error { @@ -96,7 +97,7 @@ func runUninstall(ctx context.Context, cmd *base.Command, args []string) error { return errors.New("nothing to uninstall") } - m, err := cache.NewManager(cfg.CacheDir()) + m, err := workspace.CacheMgr() if err != nil { base.SetExitStatus(base.SCacheError) return err diff --git a/cmd/slackdump/internal/format/format.go b/cmd/slackdump/internal/format/format.go index 583a13c9..2b0f2940 100644 --- a/cmd/slackdump/internal/format/format.go +++ b/cmd/slackdump/internal/format/format.go @@ -16,6 +16,7 @@ import ( "github.com/rusq/slack" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/bootstrap" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" @@ -263,7 +264,7 @@ var errNoMatch = errors.New("no matching users") func searchCache(ctx context.Context, cacheDir string, ids []string) ([]slack.User, error) { _, task := trace.NewTask(ctx, "searchCache") defer task.End() - m, err := cache.NewManager(cacheDir) + m, err := workspace.CacheMgr() if err != nil { return nil, err } diff --git a/cmd/slackdump/internal/list/common.go b/cmd/slackdump/internal/list/common.go index 187b36f5..43a17eb9 100644 --- a/cmd/slackdump/internal/list/common.go +++ b/cmd/slackdump/internal/list/common.go @@ -12,6 +12,7 @@ import ( "github.com/rusq/slackdump/v3" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace" "github.com/rusq/slackdump/v3/internal/cache" "github.com/rusq/slackdump/v3/internal/format" "github.com/rusq/slackdump/v3/types" @@ -86,7 +87,7 @@ func addCommonFlags(fs *flag.FlagSet) { } func list[T any](ctx context.Context, sess *slackdump.Session, l lister[T], filename string) error { - m, err := cache.NewManager(cfg.CacheDir()) + m, err := workspace.CacheMgr() if err != nil { return err } diff --git a/cmd/slackdump/internal/workspace/del.go b/cmd/slackdump/internal/workspace/del.go index 37aca709..b208783c 100644 --- a/cmd/slackdump/internal/workspace/del.go +++ b/cmd/slackdump/internal/workspace/del.go @@ -38,7 +38,7 @@ var ( ) func runWspDel(ctx context.Context, cmd *base.Command, args []string) error { - m, err := cache.NewManager(cfg.CacheDir()) + m, err := CacheMgr() if err != nil { base.SetExitStatus(base.SCacheError) return err diff --git a/cmd/slackdump/internal/workspace/import.go b/cmd/slackdump/internal/workspace/import.go index 0d431437..f8fb6331 100644 --- a/cmd/slackdump/internal/workspace/import.go +++ b/cmd/slackdump/internal/workspace/import.go @@ -8,7 +8,6 @@ import ( "github.com/rusq/slackdump/v3/auth" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" - "github.com/rusq/slackdump/v3/internal/cache" ) //go:embed assets/import.md @@ -44,7 +43,7 @@ func importFile(ctx context.Context, filename string) error { base.SetExitStatus(base.SUserError) return err } - m, err := cache.NewManager(cfg.CacheDir()) + m, err := CacheMgr() if err != nil { base.SetExitStatus(base.SCacheError) return err diff --git a/cmd/slackdump/internal/workspace/list.go b/cmd/slackdump/internal/workspace/list.go index f8b9336e..9299646e 100644 --- a/cmd/slackdump/internal/workspace/list.go +++ b/cmd/slackdump/internal/workspace/list.go @@ -41,7 +41,7 @@ func init() { } func runList(ctx context.Context, cmd *base.Command, args []string) error { - m, err := cache.NewManager(cfg.CacheDir()) + m, err := CacheMgr() if err != nil { base.SetExitStatus(base.SCacheError) return err @@ -128,7 +128,7 @@ func printBare(_ context.Context, w io.Writer, _ manager, current string, worksp } func wspInfo(ctx context.Context, m manager, current string, wsps []string) [][]string { - var rows = [][]string{} + rows := [][]string{} var ( wg sync.WaitGroup diff --git a/cmd/slackdump/internal/workspace/new.go b/cmd/slackdump/internal/workspace/new.go index dd5802d5..76bb10bd 100644 --- a/cmd/slackdump/internal/workspace/new.go +++ b/cmd/slackdump/internal/workspace/new.go @@ -22,7 +22,7 @@ var CmdWspNew = &base.Command{ UsageLine: baseCommand + " new [flags] ", Short: "authenticate in a Slack Workspace", Long: newMD, - FlagMask: flagmask &^ cfg.OmitAuthFlags, // only auth flags. + FlagMask: flagmask &^ cfg.OmitAuthFlags, // only auth and machine-id override flags. PrintFlags: true, Wizard: workspaceui.WorkspaceNew, } @@ -39,8 +39,7 @@ func init() { // runWspNew authenticates in the new workspace. func runWspNew(ctx context.Context, cmd *base.Command, args []string) error { - m, err := cache.NewManager( - cfg.CacheDir(), + m, err := CacheMgr( cache.WithAuthOpts( auth.BrowserWithBrowser(cfg.Browser), auth.BrowserWithTimeout(cfg.LoginTimeout), diff --git a/cmd/slackdump/internal/workspace/select.go b/cmd/slackdump/internal/workspace/select.go index 8ae34660..ede13472 100644 --- a/cmd/slackdump/internal/workspace/select.go +++ b/cmd/slackdump/internal/workspace/select.go @@ -41,7 +41,7 @@ func runSelect(ctx context.Context, cmd *base.Command, args []string) error { base.SetExitStatus(base.SInvalidParameters) return cache.ErrNameRequired } - m, err := cache.NewManager(cfg.CacheDir()) + m, err := CacheMgr() if err != nil { base.SetExitStatus(base.SCacheError) return fmt.Errorf("unable to initialise cache: %s", err) diff --git a/cmd/slackdump/internal/workspace/wiz_select.go b/cmd/slackdump/internal/workspace/wiz_select.go index b9175f73..1760aa3b 100644 --- a/cmd/slackdump/internal/workspace/wiz_select.go +++ b/cmd/slackdump/internal/workspace/wiz_select.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace/workspaceui" @@ -16,7 +17,7 @@ import ( // TODO: organise as a self-sufficient model with proper error handling. func wizSelect(ctx context.Context, cmd *base.Command, args []string) error { - m, err := cache.NewManager(cfg.CacheDir()) + m, err := CacheMgr() if err != nil { base.SetExitStatus(base.SCacheError) return err @@ -61,8 +62,7 @@ func wizSelect(ctx context.Context, cmd *base.Command, args []string) error { // newWspSelectModel creates a new workspace selection model. func newWspSelectModel(ctx context.Context, m *cache.Manager) (tea.Model, error) { - - var refreshFn = func() (cols []table.Column, rows []table.Row, err error) { + refreshFn := func() (cols []table.Column, rows []table.Row, err error) { cols = []table.Column{ {Title: "C", Width: 1}, {Title: "Name", Width: 14}, diff --git a/cmd/slackdump/internal/workspace/workspace.go b/cmd/slackdump/internal/workspace/workspace.go index 0dbb5106..15afe356 100644 --- a/cmd/slackdump/internal/workspace/workspace.go +++ b/cmd/slackdump/internal/workspace/workspace.go @@ -103,7 +103,7 @@ func AuthCurrent(ctx context.Context, cacheDir string, overrideWsp string, usePl // configuration values. If cfg.Workspace is set, it checks if the workspace // cfg.Workspace exists in the directory dir, and returns it. func Current(cacheDir string, override string) (wsp string, err error) { - m, err := cache.NewManager(cacheDir) + m, err := cache.NewManager(cacheDir, cache.WithMachineID(cfg.MachineIDOvr)) if err != nil { return "", err } @@ -131,7 +131,7 @@ var yesno = base.YesNo // credentials in the cacheDir. It returns ErrNotExists if the workspace // doesn't exist in the cacheDir. func authWsp(ctx context.Context, cacheDir string, wsp string, usePlaywright bool) (auth.Provider, error) { - m, err := cache.NewManager(cacheDir) + m, err := cache.NewManager(cacheDir, cache.WithMachineID(cfg.MachineIDOvr)) if err != nil { return nil, err } @@ -145,3 +145,8 @@ func authWsp(ctx context.Context, cacheDir string, wsp string, usePlaywright boo } return prov, nil } + +func CacheMgr(opts ...cache.Option) (*cache.Manager, error) { + opts = append([]cache.Option{cache.WithMachineID(cfg.MachineIDOvr)}, opts...) + return cache.NewManager(cfg.CacheDir(), opts...) +} diff --git a/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go b/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go index 8d0f86e7..3a1a1da4 100644 --- a/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go +++ b/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go @@ -54,12 +54,12 @@ func ShowUI(ctx context.Context, opts ...UIOption) error { actExit = "exit" ) - mgr, err := cache.NewManager(cfg.CacheDir()) + mgr, err := cache.NewManager(cfg.CacheDir(), cache.WithMachineID(cfg.MachineIDOvr)) // avoiding import cycle if err != nil { return err } - var uiOpts = options{ + uiOpts := options{ title: "New Workspace", } for _, o := range opts { @@ -113,7 +113,7 @@ func ShowUI(ctx context.Context, opts ...UIOption) error { } // new workspace methods - var methods = map[string]func(context.Context, manager) error{ + methods := map[string]func(context.Context, manager) error{ actLogin: brwsLogin(&brwsOpts), actToken: prgTokenCookie, actTokenFile: prgTokenCookieFile, diff --git a/internal/cache/auth.go b/internal/cache/auth.go index 134624ca..89eb6d08 100644 --- a/internal/cache/auth.go +++ b/internal/cache/auth.go @@ -19,7 +19,7 @@ import ( const ezLogin = "EZ-Login 3000" -//go:generate mockgen -source=auth.go -destination=../../mocks/mock_appauth/mock_appauth.go Credentials,createOpener +//go:generate mockgen -source=auth.go -destination=../mocks/mock_cache/mock_cache.go Credentials,createOpener //go:generate mockgen -destination=../mocks/mock_io/mock_io.go io ReadCloser,WriteCloser // isWSL is true if we're running in the WSL environment @@ -210,15 +210,22 @@ func (a authenticator) tryLoad(ctx context.Context, filename string) (auth.Provi return prov, nil } +var ErrFailed = errors.New("failed to load stored credentials") + // loadCreds loads the encrypted credentials from the file. func loadCreds(ct createOpener, filename string) (auth.Provider, error) { f, err := ct.Open(filename) if err != nil { - return nil, errors.New("failed to load stored credentials") + return nil, ErrFailed } defer f.Close() - return auth.Load(f) + p, err := auth.Load(f) + if err != nil { + slog.Debug("failed to load credentials, possibly mismatched machine ID", "err", err) + return nil, ErrFailed + } + return p, nil } // saveCreds encrypts and saves the credentials. diff --git a/internal/cache/auth_test.go b/internal/cache/auth_test.go index 957cea81..a1954b84 100644 --- a/internal/cache/auth_test.go +++ b/internal/cache/auth_test.go @@ -16,11 +16,11 @@ import ( "github.com/rusq/slackdump/v3/auth" "github.com/rusq/slackdump/v3/internal/fixtures" - "github.com/rusq/slackdump/v3/internal/mocks/mock_appauth" + "github.com/rusq/slackdump/v3/internal/mocks/mock_cache" "github.com/rusq/slackdump/v3/internal/mocks/mock_io" ) -func Test_isExistingFile(t *testing.T) { +func Test_exists(t *testing.T) { testfile := filepath.Join(t.TempDir(), "cookies.txt") if err := os.WriteFile(testfile, []byte("blah"), 0o600); err != nil { t.Fatal(err) @@ -39,7 +39,7 @@ func Test_isExistingFile(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := exists.cookie); got != tt.want { + if got := exists(tt.args.cookie); got != tt.want { t.Errorf("isExistingFile() = %v, want %v", got, tt.want) } }) @@ -138,7 +138,7 @@ func TestInitProvider(t *testing.T) { tests := []struct { name string args args - expect func(m *mock_appauth.MockCredentials) + expect func(m *mock_cache.MockCredentials) authTestErr error want auth.Provider wantErr bool @@ -146,7 +146,7 @@ func TestInitProvider(t *testing.T) { { "empty creds, no errors", args{context.Background(), testDir, "wsp"}, - func(m *mock_appauth.MockCredentials) { + func(m *mock_cache.MockCredentials) { m.EXPECT().IsEmpty().Return(false) m.EXPECT(). AuthProvider(gomock.Any(), "wsp"). @@ -159,7 +159,7 @@ func TestInitProvider(t *testing.T) { { "creds empty, tryLoad succeeds", args{context.Background(), testDir, "wsp"}, - func(m *mock_appauth.MockCredentials) { + func(m *mock_cache.MockCredentials) { m.EXPECT().IsEmpty().Return(true) }, nil, @@ -169,7 +169,7 @@ func TestInitProvider(t *testing.T) { { "creds empty, tryLoad fails", args{context.Background(), testDir, "wsp"}, - func(m *mock_appauth.MockCredentials) { + func(m *mock_cache.MockCredentials) { m.EXPECT().IsEmpty().Return(true) m.EXPECT().AuthProvider(gomock.Any(), "wsp").Return(returnedProv, nil) }, @@ -180,7 +180,7 @@ func TestInitProvider(t *testing.T) { { "creds non-empty, provider failed", args{context.Background(), testDir, "wsp"}, - func(m *mock_appauth.MockCredentials) { + func(m *mock_cache.MockCredentials) { m.EXPECT().IsEmpty().Return(false) m.EXPECT().AuthProvider(gomock.Any(), "wsp").Return(nil, errors.New("authProvider failed")) }, @@ -191,7 +191,7 @@ func TestInitProvider(t *testing.T) { { "creds non-empty, provider succeeds, save succeeds", args{context.Background(), testDir, "wsp"}, - func(m *mock_appauth.MockCredentials) { + func(m *mock_cache.MockCredentials) { m.EXPECT().IsEmpty().Return(false) m.EXPECT().AuthProvider(gomock.Any(), "wsp").Return(returnedProv, nil) }, @@ -202,7 +202,7 @@ func TestInitProvider(t *testing.T) { { "creds non-empty, provider succeeds, save fails", args{context.Background(), t.TempDir() + "$", "wsp"}, - func(m *mock_appauth.MockCredentials) { + func(m *mock_cache.MockCredentials) { m.EXPECT().IsEmpty().Return(false) m.EXPECT().AuthProvider(gomock.Any(), "wsp").Return(returnedProv, nil) }, @@ -227,7 +227,7 @@ func TestInitProvider(t *testing.T) { t.Fatal(err) } - mc := mock_appauth.NewMockCredentials(gomock.NewController(t)) + mc := mock_cache.NewMockCredentials(gomock.NewController(t)) tt.expect(mc) auther := newAuthenticator(tt.args.cacheDir, "") @@ -333,14 +333,14 @@ func Test_loadCreds(t *testing.T) { tests := []struct { name string args args - expect func(mco *mock_appauth.Mockcontainer, mrc *mock_io.MockReadCloser) + expect func(mco *mock_cache.MockcreateOpener, mrc *mock_io.MockReadCloser) want auth.Provider wantErr bool }{ { "all ok", args{"fakefile.ext"}, - func(mco *mock_appauth.Mockcontainer, mrc *mock_io.MockReadCloser) { + func(mco *mock_cache.MockcreateOpener, mrc *mock_io.MockReadCloser) { readCall := mrc.EXPECT(). Read(gomock.Any()). DoAndReturn(func(b []byte) (int, error) { @@ -358,7 +358,7 @@ func Test_loadCreds(t *testing.T) { { "auth.Read error", args{"fakefile.ext"}, - func(mco *mock_appauth.Mockcontainer, mrc *mock_io.MockReadCloser) { + func(mco *mock_cache.MockcreateOpener, mrc *mock_io.MockReadCloser) { readCall := mrc.EXPECT(). Read(gomock.Any()). Return(0, errors.New("auth.Read error")) @@ -368,13 +368,13 @@ func Test_loadCreds(t *testing.T) { Open("fakefile.ext"). Return(mrc, nil) }, - auth.ValueAuth{}, + nil, true, }, { "read error", args{"fakefile.ext"}, - func(mco *mock_appauth.Mockcontainer, mrc *mock_io.MockReadCloser) { + func(mco *mock_cache.MockcreateOpener, mrc *mock_io.MockReadCloser) { mco.EXPECT(). Open("fakefile.ext"). Return(nil, errors.New("it was at this moment that test framework knew: it fucked up")) @@ -386,7 +386,7 @@ func Test_loadCreds(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) - mco := mock_appauth.NewMockcontainer(ctrl) + mco := mock_cache.NewMockcreateOpener(ctrl) mrc := mock_io.NewMockReadCloser(ctrl) tt.expect(mco, mrc) @@ -418,13 +418,13 @@ func Test_saveCreds(t *testing.T) { tests := []struct { name string args args - expect func(m *mock_appauth.Mockcontainer, mwc *mock_io.MockWriteCloser) + expect func(m *mock_cache.MockcreateOpener, mwc *mock_io.MockWriteCloser) wantErr bool }{ { "all ok", args{filename: "filename.ext", p: testProv}, - func(m *mock_appauth.Mockcontainer, mwc *mock_io.MockWriteCloser) { + func(m *mock_cache.MockcreateOpener, mwc *mock_io.MockWriteCloser) { wc := mwc.EXPECT().Write(testProvBytes).Return(len(testProvBytes), nil) mwc.EXPECT().Close().After(wc).Return(nil) @@ -435,7 +435,7 @@ func Test_saveCreds(t *testing.T) { { "create fails", args{filename: "filename.ext", p: testProv}, - func(m *mock_appauth.Mockcontainer, mwc *mock_io.MockWriteCloser) { + func(m *mock_cache.MockcreateOpener, mwc *mock_io.MockWriteCloser) { m.EXPECT().Create("filename.ext").Return(nil, errors.New("create fail")) }, true, @@ -443,7 +443,7 @@ func Test_saveCreds(t *testing.T) { { "write fails", args{filename: "filename.ext", p: testProv}, - func(m *mock_appauth.Mockcontainer, mwc *mock_io.MockWriteCloser) { + func(m *mock_cache.MockcreateOpener, mwc *mock_io.MockWriteCloser) { wc := mwc.EXPECT().Write(testProvBytes).Return(0, errors.New("write fail")) mwc.EXPECT().Close().After(wc).Return(nil) @@ -455,7 +455,7 @@ func Test_saveCreds(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) - mco := mock_appauth.NewMockcontainer(ctrl) + mco := mock_cache.NewMockcreateOpener(ctrl) mwc := mock_io.NewMockWriteCloser(ctrl) tt.expect(mco, mwc) diff --git a/internal/cache/manager.go b/internal/cache/manager.go index 86611155..26d998a9 100644 --- a/internal/cache/manager.go +++ b/internal/cache/manager.go @@ -151,7 +151,7 @@ func (m *Manager) Auth(ctx context.Context, name string, c Credentials) (auth.Pr return a.initProvider(ctx, m.filename(name), name, c, m.authOptions...) } -// LoadProvider loads the file from disk without any checks. +// LoadProvider loads the file from disk without any logical validation. func (m *Manager) LoadProvider(name string) (auth.Provider, error) { filer := encryptedFile{machineID: m.machineID} return loadCreds(filer, m.filepath(name)) diff --git a/internal/cache/manager_test.go b/internal/cache/manager_test.go index 627340e5..62e1e3f8 100644 --- a/internal/cache/manager_test.go +++ b/internal/cache/manager_test.go @@ -10,11 +10,12 @@ import ( "testing" "github.com/rusq/slack" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "github.com/rusq/slackdump/v3/auth" "github.com/rusq/slackdump/v3/internal/fixtures" "github.com/rusq/slackdump/v3/internal/mocks/mock_auth" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" ) func Test_currentWsp(t *testing.T) { @@ -50,6 +51,7 @@ func Test_currentWsp(t *testing.T) { } func prepareDir(t *testing.T, dir string) { + t.Helper() fixtures.PrepareDir(t, dir, "dummy", fixtures.WorkspaceFiles...) } @@ -251,3 +253,47 @@ func TestManager_CreateAndSelect(t *testing.T) { }) } } + +func TestManager_LoadProvider(t *testing.T) { + t.Run("loads provider", func(t *testing.T) { + dir := t.TempDir() + m := &Manager{ + dir: dir, + } + prov, err := auth.NewValueAuth(fixtures.TestClientToken, "xoxd-1234567890-1234567890-1234567890-1234567890") + assert.NoError(t, err) + + m.saveProvider("test.bin", prov) + got, err := m.LoadProvider("test.bin") + assert.NoError(t, err) + assert.Equal(t, prov, got) + }) + t.Run("encrypted with different machineID", func(t *testing.T) { + dir := t.TempDir() + m := &Manager{ + dir: dir, + } + prov, err := auth.NewValueAuth(fixtures.TestClientToken, "xoxd-1234567890-1234567890-1234567890-1234567890") + assert.NoError(t, err) + + m.saveProvider("test.bin", prov) + m.machineID = "1234567890" + got, err := m.LoadProvider("test.bin") + assert.Error(t, err) + assert.NotEqual(t, prov, got) + }) + t.Run("encrypted with the same machine ID override", func(t *testing.T) { + dir := t.TempDir() + m := &Manager{ + dir: dir, + machineID: "1234567890", + } + prov, err := auth.NewValueAuth(fixtures.TestClientToken, "xoxd-1234567890-1234567890-1234567890-1234567890") + assert.NoError(t, err) + + m.saveProvider("test.bin", prov) + got, err := m.LoadProvider("test.bin") + assert.NoError(t, err) + assert.Equal(t, prov, got) + }) +} diff --git a/internal/fixtures/export.go b/internal/fixtures/export.go index 2c09d4c6..3d0200de 100644 --- a/internal/fixtures/export.go +++ b/internal/fixtures/export.go @@ -2,7 +2,6 @@ package fixtures import ( "embed" - _ "embed" ) const TestConversationExportJSON = `{ diff --git a/internal/mocks/mock_appauth/mock_appauth.go b/internal/mocks/mock_appauth/mock_appauth.go deleted file mode 100644 index 782fcd5b..00000000 --- a/internal/mocks/mock_appauth/mock_appauth.go +++ /dev/null @@ -1,124 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: auth.go - -// Package mock_appauth is a generated GoMock package. -package mock_appauth - -import ( - context "context" - io "io" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" - auth "github.com/rusq/slackdump/v3/auth" -) - -// MockCredentials is a mock of Credentials interface. -type MockCredentials struct { - ctrl *gomock.Controller - recorder *MockCredentialsMockRecorder -} - -// MockCredentialsMockRecorder is the mock recorder for MockCredentials. -type MockCredentialsMockRecorder struct { - mock *MockCredentials -} - -// NewMockCredentials creates a new mock instance. -func NewMockCredentials(ctrl *gomock.Controller) *MockCredentials { - mock := &MockCredentials{ctrl: ctrl} - mock.recorder = &MockCredentialsMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockCredentials) EXPECT() *MockCredentialsMockRecorder { - return m.recorder -} - -// AuthProvider mocks base method. -func (m *MockCredentials) AuthProvider(ctx context.Context, workspace string, opts ...auth.Option) (auth.Provider, error) { - m.ctrl.T.Helper() - varargs := []interface{}{ctx, workspace} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "AuthProvider", varargs...) - ret0, _ := ret[0].(auth.Provider) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AuthProvider indicates an expected call of AuthProvider. -func (mr *MockCredentialsMockRecorder) AuthProvider(ctx, workspace interface{}, opts ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx, workspace}, opts...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthProvider", reflect.TypeOf((*MockCredentials)(nil).AuthProvider), varargs...) -} - -// IsEmpty mocks base method. -func (m *MockCredentials) IsEmpty() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsEmpty") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsEmpty indicates an expected call of IsEmpty. -func (mr *MockCredentialsMockRecorder) IsEmpty() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEmpty", reflect.TypeOf((*MockCredentials)(nil).IsEmpty)) -} - -// Mockcontainer is a mock of container interface. -type Mockcontainer struct { - ctrl *gomock.Controller - recorder *MockcontainerMockRecorder -} - -// MockcontainerMockRecorder is the mock recorder for Mockcontainer. -type MockcontainerMockRecorder struct { - mock *Mockcontainer -} - -// NewMockcontainer creates a new mock instance. -func NewMockcontainer(ctrl *gomock.Controller) *Mockcontainer { - mock := &Mockcontainer{ctrl: ctrl} - mock.recorder = &MockcontainerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *Mockcontainer) EXPECT() *MockcontainerMockRecorder { - return m.recorder -} - -// Create mocks base method. -func (m *Mockcontainer) Create(filename string) (io.WriteCloser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", filename) - ret0, _ := ret[0].(io.WriteCloser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Create indicates an expected call of Create. -func (mr *MockcontainerMockRecorder) Create(filename interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*Mockcontainer)(nil).Create), filename) -} - -// Open mocks base method. -func (m *Mockcontainer) Open(filename string) (io.ReadCloser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Open", filename) - ret0, _ := ret[0].(io.ReadCloser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Open indicates an expected call of Open. -func (mr *MockcontainerMockRecorder) Open(filename interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*Mockcontainer)(nil).Open), filename) -} diff --git a/mocks/mock_appauth/mock_appauth.go b/internal/mocks/mock_cache/mock_cache.go similarity index 74% rename from mocks/mock_appauth/mock_appauth.go rename to internal/mocks/mock_cache/mock_cache.go index 4315f735..48e8e9d0 100644 --- a/mocks/mock_appauth/mock_appauth.go +++ b/internal/mocks/mock_cache/mock_cache.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -source=auth.go -destination=../../mocks/mock_appauth/mock_appauth.go Credentials,createOpener +// mockgen -source=auth.go -destination=../mocks/mock_cache/mock_cache.go Credentials,createOpener // // Package mock_cache is a generated GoMock package. @@ -76,32 +76,32 @@ func (mr *MockCredentialsMockRecorder) IsEmpty() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEmpty", reflect.TypeOf((*MockCredentials)(nil).IsEmpty)) } -// Mockcontainer is a mock of container interface. -type Mockcontainer struct { +// MockcreateOpener is a mock of createOpener interface. +type MockcreateOpener struct { ctrl *gomock.Controller - recorder *MockcontainerMockRecorder + recorder *MockcreateOpenerMockRecorder isgomock struct{} } -// MockcontainerMockRecorder is the mock recorder for Mockcontainer. -type MockcontainerMockRecorder struct { - mock *Mockcontainer +// MockcreateOpenerMockRecorder is the mock recorder for MockcreateOpener. +type MockcreateOpenerMockRecorder struct { + mock *MockcreateOpener } -// NewMockcontainer creates a new mock instance. -func NewMockcontainer(ctrl *gomock.Controller) *Mockcontainer { - mock := &Mockcontainer{ctrl: ctrl} - mock.recorder = &MockcontainerMockRecorder{mock} +// NewMockcreateOpener creates a new mock instance. +func NewMockcreateOpener(ctrl *gomock.Controller) *MockcreateOpener { + mock := &MockcreateOpener{ctrl: ctrl} + mock.recorder = &MockcreateOpenerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *Mockcontainer) EXPECT() *MockcontainerMockRecorder { +func (m *MockcreateOpener) EXPECT() *MockcreateOpenerMockRecorder { return m.recorder } // Create mocks base method. -func (m *Mockcontainer) Create(filename string) (io.WriteCloser, error) { +func (m *MockcreateOpener) Create(filename string) (io.WriteCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", filename) ret0, _ := ret[0].(io.WriteCloser) @@ -110,13 +110,13 @@ func (m *Mockcontainer) Create(filename string) (io.WriteCloser, error) { } // Create indicates an expected call of Create. -func (mr *MockcontainerMockRecorder) Create(filename any) *gomock.Call { +func (mr *MockcreateOpenerMockRecorder) Create(filename any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*Mockcontainer)(nil).Create), filename) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockcreateOpener)(nil).Create), filename) } // Open mocks base method. -func (m *Mockcontainer) Open(filename string) (io.ReadCloser, error) { +func (m *MockcreateOpener) Open(filename string) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Open", filename) ret0, _ := ret[0].(io.ReadCloser) @@ -125,7 +125,7 @@ func (m *Mockcontainer) Open(filename string) (io.ReadCloser, error) { } // Open indicates an expected call of Open. -func (mr *MockcontainerMockRecorder) Open(filename any) *gomock.Call { +func (mr *MockcreateOpenerMockRecorder) Open(filename any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*Mockcontainer)(nil).Open), filename) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockcreateOpener)(nil).Open), filename) } diff --git a/internal/mocks/mock_dl/mock_exporter.go b/internal/mocks/mock_dl/mock_exporter.go deleted file mode 100644 index 11dd5071..00000000 --- a/internal/mocks/mock_dl/mock_exporter.go +++ /dev/null @@ -1,80 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/rusq/slackdump/v3/internal/structures/files/dl (interfaces: Exporter) -// -// Generated by this command: -// -// mockgen -destination ../../../../internal/mocks/mock_dl/mock_exporter.go github.com/rusq/slackdump/v3/internal/structures/files/dl Exporter -// - -// Package mock_dl is a generated GoMock package. -package mock_dl - -import ( - context "context" - reflect "reflect" - - slackdump "github.com/rusq/slackdump/v3" - gomock "go.uber.org/mock/gomock" -) - -// MockExporter is a mock of Exporter interface. -type MockExporter struct { - ctrl *gomock.Controller - recorder *MockExporterMockRecorder - isgomock struct{} -} - -// MockExporterMockRecorder is the mock recorder for MockExporter. -type MockExporterMockRecorder struct { - mock *MockExporter -} - -// NewMockExporter creates a new mock instance. -func NewMockExporter(ctrl *gomock.Controller) *MockExporter { - mock := &MockExporter{ctrl: ctrl} - mock.recorder = &MockExporterMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockExporter) EXPECT() *MockExporterMockRecorder { - return m.recorder -} - -// ProcessFunc mocks base method. -func (m *MockExporter) ProcessFunc(channelName string) slackdump.ProcessFunc { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProcessFunc", channelName) - ret0, _ := ret[0].(slackdump.ProcessFunc) - return ret0 -} - -// ProcessFunc indicates an expected call of ProcessFunc. -func (mr *MockExporterMockRecorder) ProcessFunc(channelName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessFunc", reflect.TypeOf((*MockExporter)(nil).ProcessFunc), channelName) -} - -// Start mocks base method. -func (m *MockExporter) Start(ctx context.Context) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Start", ctx) -} - -// Start indicates an expected call of Start. -func (mr *MockExporterMockRecorder) Start(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockExporter)(nil).Start), ctx) -} - -// Stop mocks base method. -func (m *MockExporter) Stop() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Stop") -} - -// Stop indicates an expected call of Stop. -func (mr *MockExporterMockRecorder) Stop() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockExporter)(nil).Stop)) -} diff --git a/internal/mocks/mock_fsadapter/mock_fs.go b/internal/mocks/mock_fsadapter/mock_fs.go deleted file mode 100644 index 25b23dd8..00000000 --- a/internal/mocks/mock_fsadapter/mock_fs.go +++ /dev/null @@ -1,65 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/rusq/fsadapter (interfaces: FS) - -// Package mock_fsadapter is a generated GoMock package. -package mock_fsadapter - -import ( - io "io" - fs "io/fs" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockFS is a mock of FS interface. -type MockFS struct { - ctrl *gomock.Controller - recorder *MockFSMockRecorder -} - -// MockFSMockRecorder is the mock recorder for MockFS. -type MockFSMockRecorder struct { - mock *MockFS -} - -// NewMockFS creates a new mock instance. -func NewMockFS(ctrl *gomock.Controller) *MockFS { - mock := &MockFS{ctrl: ctrl} - mock.recorder = &MockFSMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockFS) EXPECT() *MockFSMockRecorder { - return m.recorder -} - -// Create mocks base method. -func (m *MockFS) Create(arg0 string) (io.WriteCloser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", arg0) - ret0, _ := ret[0].(io.WriteCloser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Create indicates an expected call of Create. -func (mr *MockFSMockRecorder) Create(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFS)(nil).Create), arg0) -} - -// WriteFile mocks base method. -func (m *MockFS) WriteFile(arg0 string, arg1 []byte, arg2 fs.FileMode) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "WriteFile", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// WriteFile indicates an expected call of WriteFile. -func (mr *MockFSMockRecorder) WriteFile(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteFile", reflect.TypeOf((*MockFS)(nil).WriteFile), arg0, arg1, arg2) -} From d04ba68df1c737f7d779d7f7ca5f671ee586e26b Mon Sep 17 00:00:00 2001 From: Rustam Gilyazov <16064414+rusq@users.noreply.github.com> Date: Sat, 18 Jan 2025 22:16:00 +1000 Subject: [PATCH 5/5] PR review --- cmd/slackdump/internal/format/format.go | 3 +-- internal/cache/auth.go | 2 +- internal/cache/auth_test.go | 4 ++-- internal/cache/cache.go | 18 +++++++++++++----- internal/cache/channelcache.go | 9 +++++---- internal/cache/channelcache_test.go | 6 ++++-- internal/cache/manager.go | 8 ++++---- internal/cache/usercache.go | 8 ++++---- internal/cache/usercache_test.go | 9 ++++++--- 9 files changed, 40 insertions(+), 27 deletions(-) diff --git a/cmd/slackdump/internal/format/format.go b/cmd/slackdump/internal/format/format.go index 2b0f2940..0e907369 100644 --- a/cmd/slackdump/internal/format/format.go +++ b/cmd/slackdump/internal/format/format.go @@ -16,7 +16,6 @@ import ( "github.com/rusq/slack" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/bootstrap" - "github.com/rusq/slackdump/v3/cmd/slackdump/internal/workspace" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/cfg" "github.com/rusq/slackdump/v3/cmd/slackdump/internal/golang/base" @@ -264,7 +263,7 @@ var errNoMatch = errors.New("no matching users") func searchCache(ctx context.Context, cacheDir string, ids []string) ([]slack.User, error) { _, task := trace.NewTask(ctx, "searchCache") defer task.End() - m, err := workspace.CacheMgr() + m, err := cache.NewManager(cacheDir, cache.WithMachineID(cfg.MachineIDOvr)) if err != nil { return nil, err } diff --git a/internal/cache/auth.go b/internal/cache/auth.go index 89eb6d08..91bd7a75 100644 --- a/internal/cache/auth.go +++ b/internal/cache/auth.go @@ -252,7 +252,7 @@ type createOpener interface { var _ createOpener = encryptedFile{} -// encryptedFile is the encrypted file container. +// encryptedFile is the encrypted file wrapper. type encryptedFile struct { // machineID is the machine ID override. If it is empty, the actual machine // ID is used. diff --git a/internal/cache/auth_test.go b/internal/cache/auth_test.go index a1954b84..ed72d00d 100644 --- a/internal/cache/auth_test.go +++ b/internal/cache/auth_test.go @@ -222,8 +222,8 @@ func TestInitProvider(t *testing.T) { // resetting credentials credsFile := filepath.Join(testDir, defCredsFile) - container := encryptedFile{} - if err := saveCreds(container, credsFile, storedProv); err != nil { + co := encryptedFile{} + if err := saveCreds(co, credsFile, storedProv); err != nil { t.Fatal(err) } diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 50b7b228..f9ee6a74 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -82,10 +82,14 @@ func writeSlice[T any](w io.Writer, tt []T) error { // save saves the users to a file, naming the file based on the filename // and the suffix. The file will be saved in the cache directory. -func save[T any](cacheDir, filename string, suffix string, uu []T) error { +func save[T any](cacheDir, filename string, suffix string, uu []T, machineID string) error { filename = makeCacheFilename(cacheDir, filename, suffix) - f, err := encio.Create(filename) + var opts []encio.Option + if machineID != "" { + opts = append(opts, encio.WithID(machineID)) + } + f, err := encio.Create(filename, opts...) if err != nil { return fmt.Errorf("failed to create file %s: %w", filename, err) } @@ -101,7 +105,7 @@ func save[T any](cacheDir, filename string, suffix string, uu []T) error { // it as a slice of T. func read[T any](r io.Reader) ([]T, error) { dec := json.NewDecoder(r) - var tt = make([]T, 0, 500) // 500 T. reasonable? + tt := make([]T, 0, 500) // 500 T. reasonable? for { var t T if err := dec.Decode(&t); err != nil { @@ -117,14 +121,18 @@ func read[T any](r io.Reader) ([]T, error) { // load loads the data from the file in the cache directory, and returns // the data as a slice of T. -func load[T any](cacheDir, filename string, suffix string, maxAge time.Duration) ([]T, error) { +func load[T any](cacheDir, filename, suffix string, maxAge time.Duration, machineID string) ([]T, error) { + var opts []encio.Option + if machineID != "" { + opts = append(opts, encio.WithID(machineID)) + } filename = makeCacheFilename(cacheDir, filename, suffix) if err := checkCacheFile(filename, maxAge); err != nil { return nil, fmt.Errorf("%s: %w", filename, err) } - f, err := encio.Open(filename) + f, err := encio.Open(filename, opts...) if err != nil { return nil, fmt.Errorf("failed to open %s: %w", filename, err) } diff --git a/internal/cache/channelcache.go b/internal/cache/channelcache.go index 3b1d3b3f..0154239d 100644 --- a/internal/cache/channelcache.go +++ b/internal/cache/channelcache.go @@ -4,13 +4,14 @@ import ( "time" "github.com/rusq/slack" + "github.com/rusq/slackdump/v3/types" ) // loadChannels tries to load channels from the file. If the file does not exist // or is older than maxAge, it returns an error. -func loadChannels(dirname, filename string, suffix string, maxAge time.Duration) ([]slack.Channel, error) { - uu, err := load[slack.Channel](dirname, filename, suffix, maxAge) +func (m *Manager) loadChannels(dirname, filename string, suffix string, maxAge time.Duration) ([]slack.Channel, error) { + uu, err := load[slack.Channel](dirname, filename, suffix, maxAge, m.machineID) if err != nil { return nil, err } @@ -19,6 +20,6 @@ func loadChannels(dirname, filename string, suffix string, maxAge time.Duration) // saveChannels saves channels to a file, naming the file based on the // filename and the suffix. The file will be saved in the dirname. -func saveChannels(dirname, filename string, suffix string, cc []slack.Channel) error { - return save(dirname, filename, suffix, cc) +func (m *Manager) saveChannels(dirname, filename string, suffix string, cc []slack.Channel) error { + return save(dirname, filename, suffix, cc, m.machineID) } diff --git a/internal/cache/channelcache_test.go b/internal/cache/channelcache_test.go index bdfc6569..d26eeb11 100644 --- a/internal/cache/channelcache_test.go +++ b/internal/cache/channelcache_test.go @@ -5,9 +5,10 @@ import ( "github.com/rusq/encio" "github.com/rusq/slack" + "github.com/stretchr/testify/assert" + "github.com/rusq/slackdump/v3/internal/fixtures" "github.com/rusq/slackdump/v3/types" - "github.com/stretchr/testify/assert" ) // testChannels is a test fixture for channels. @@ -19,7 +20,8 @@ func TestSaveChannels(t *testing.T) { dir := t.TempDir() testfile := "test-chans.json" - assert.NoError(t, saveChannels(dir, testfile, testSuffix, testChannels)) + var m Manager + assert.NoError(t, m.saveChannels(dir, testfile, testSuffix, testChannels)) reopenedF, err := encio.Open(makeCacheFilename(dir, testfile, testSuffix)) if err != nil { diff --git a/internal/cache/manager.go b/internal/cache/manager.go index 26d998a9..8a88c9ca 100644 --- a/internal/cache/manager.go +++ b/internal/cache/manager.go @@ -409,22 +409,22 @@ func (m *Manager) WalkUsers(userFn func(path string, r io.Reader) error) error { // LoadUsers loads user cache file no older than maxAge for teamID. func (m *Manager) LoadUsers(teamID string, maxAge time.Duration) ([]slack.User, error) { - return loadUsers(m.dir, m.userFile, teamID, maxAge) + return m.loadUsers(m.dir, m.userFile, teamID, maxAge) } // CacheUsers saves users to user cache file for teamID. func (m *Manager) CacheUsers(teamID string, uu []slack.User) error { - return saveUsers(m.dir, m.userFile, teamID, uu) + return m.saveUsers(m.dir, m.userFile, teamID, uu) } // LoadChannels loads channel cache no older than maxAge. func (m *Manager) LoadChannels(teamID string, maxAge time.Duration) ([]slack.Channel, error) { - return loadChannels(m.dir, m.channelFile, teamID, maxAge) + return m.loadChannels(m.dir, m.channelFile, teamID, maxAge) } // CacheChannels saves channels to cache. func (m *Manager) CacheChannels(teamID string, cc []slack.Channel) error { - return saveChannels(m.dir, m.channelFile, teamID, cc) + return m.saveChannels(m.dir, m.channelFile, teamID, cc) } // CreateAndSelect creates a new workspace with the given provider and selects diff --git a/internal/cache/usercache.go b/internal/cache/usercache.go index 5aa93a6f..69069cf1 100644 --- a/internal/cache/usercache.go +++ b/internal/cache/usercache.go @@ -19,8 +19,8 @@ func ReadUsers(r io.Reader) (types.Users, error) { // loadUsers tries to load the users from the file. If the file does not exist // or is older than maxAge, it returns an error. -func loadUsers(dirname, filename string, suffix string, maxAge time.Duration) (types.Users, error) { - uu, err := load[slack.User](dirname, filename, suffix, maxAge) +func (m *Manager) loadUsers(dirname, filename string, suffix string, maxAge time.Duration) (types.Users, error) { + uu, err := load[slack.User](dirname, filename, suffix, maxAge, m.machineID) if err != nil { return nil, err } @@ -29,6 +29,6 @@ func loadUsers(dirname, filename string, suffix string, maxAge time.Duration) (t // saveUsers saves the users to a file, naming the file based on the filename // and the suffix. The file will be saved in the cache directory. -func saveUsers(dirname, filename string, suffix string, uu types.Users) error { - return save(dirname, filename, suffix, []slack.User(uu)) +func (m *Manager) saveUsers(dirname, filename string, suffix string, uu types.Users) error { + return save(dirname, filename, suffix, []slack.User(uu), m.machineID) } diff --git a/internal/cache/usercache_test.go b/internal/cache/usercache_test.go index ad6f8672..85c57621 100644 --- a/internal/cache/usercache_test.go +++ b/internal/cache/usercache_test.go @@ -25,7 +25,8 @@ func TestSaveUserCache(t *testing.T) { dir := t.TempDir() testfile := "test.json" - assert.NoError(t, saveUsers(dir, testfile, testSuffix, testUsers)) + var m Manager + assert.NoError(t, m.saveUsers(dir, testfile, testSuffix, testUsers)) reopenedF, err := encio.Open(makeCacheFilename(dir, testfile, testSuffix)) if err != nil { @@ -64,7 +65,8 @@ func TestLoadUserCache(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := loadUsers("", tt.args.filename, testSuffix, tt.args.maxAge) + var m Manager + got, err := m.loadUsers("", tt.args.filename, testSuffix, tt.args.maxAge) if (err != nil) != tt.wantErr { t.Errorf("Session.loadUserCache() error = %v, wantErr %v", err, tt.wantErr) return @@ -187,7 +189,8 @@ func gimmeTempFile(t *testing.T, dir string) string { func gimmeTempFileWithUsers(t *testing.T, dir string) string { f := gimmeTempFile(t, dir) - if err := saveUsers("", f, testSuffix, testUsers); err != nil { + var m Manager + if err := m.saveUsers("", f, testSuffix, testUsers); err != nil { t.Fatal(err) } return f