Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CUS-377: oauthtoken: refactor to single fake LoadStorer implementation #21

Merged
merged 7 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 46 additions & 60 deletions cmd/engflow_auth/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,6 @@ func (f *fakeAuth) FetchToken(ctx context.Context, authRes *oauth2.DeviceAuthRes
return nil, f.fetchTokenErr
}

type fakeStore struct {
loadToken *oauth2.Token
loadErr error
storeErr error
storeClusters []string
}

func (f *fakeStore) Load(ctx context.Context, cluster string) (*oauth2.Token, error) {
return f.loadToken, f.loadErr
}

func (f *fakeStore) Store(ctx context.Context, cluster string, token *oauth2.Token) error {
f.storeClusters = append(f.storeClusters, cluster)
return f.storeErr
}

type fakeBrowser struct {
openErr error
}
Expand All @@ -132,7 +116,7 @@ func TestRun(t *testing.T) {
wantErr string
wantStdoutContaining []string
wantStderrContaining []string
wantStoreCallsFor []string
wantStored []string
}{
{
desc: "no subcommand",
Expand Down Expand Up @@ -164,8 +148,8 @@ func TestRun(t *testing.T) {
desc: "get propagates token store error",
args: []string{"get"},
machineInput: strings.NewReader(`{"uri": "https://cluster.example.com"}`),
tokenStore: &fakeStore{
loadErr: errors.New("token_load_error"),
tokenStore: &oauthtoken.FakeLoadStorer{
LoadErr: errors.New("token_load_error"),
},
wantCode: autherr.CodeReauthRequired,
wantErr: "Please refresh credentials",
Expand All @@ -174,10 +158,12 @@ func TestRun(t *testing.T) {
desc: "get with URL expired",
args: []string{"get"},
machineInput: strings.NewReader(`{"uri": "https://cluster.example.com"}`),
tokenStore: &fakeStore{
loadToken: &oauth2.Token{
AccessToken: "access_token",
Expiry: time.Date(2024, 1, 2, 3, 4, 5, 6, time.UTC),
tokenStore: &oauthtoken.FakeLoadStorer{
Tokens: map[string]*oauth2.Token{
"cluster.example.com": {
AccessToken: "access_token",
Expiry: time.Date(2024, 1, 2, 3, 4, 5, 6, time.UTC),
},
},
},
wantCode: autherr.CodeReauthRequired,
Expand All @@ -187,10 +173,12 @@ func TestRun(t *testing.T) {
desc: "get with URL not expired",
args: []string{"get"},
machineInput: strings.NewReader(`{"uri": "https://cluster.example.com"}`),
tokenStore: &fakeStore{
loadToken: &oauth2.Token{
AccessToken: "access_token",
Expiry: expiresInFuture,
tokenStore: &oauthtoken.FakeLoadStorer{
Tokens: map[string]*oauth2.Token{
"cluster.example.com": {
AccessToken: "access_token",
Expiry: expiresInFuture,
},
},
},
wantStdoutContaining: []string{
Expand Down Expand Up @@ -237,8 +225,8 @@ func TestRun(t *testing.T) {
VerificationURIComplete: "https://cluster.example.com/with/auth/code",
},
},
tokenStore: &fakeStore{},
wantStoreCallsFor: []string{
tokenStore: oauthtoken.NewFakeLoadStorer(),
wantStored: []string{
"cluster.example.com",
"cluster.local.example.com",
},
Expand All @@ -251,15 +239,11 @@ func TestRun(t *testing.T) {
VerificationURIComplete: "https://cluster.example.com/with/auth/code",
},
},
tokenStore: &fakeStore{
storeErr: errors.New("token_store_fail"),
tokenStore: &oauthtoken.FakeLoadStorer{
StoreErr: errors.New("token_store_fail"),
},
wantCode: autherr.CodeTokenStoreFailure,
wantErr: "2 token store operation(s) failed",
wantStoreCallsFor: []string{
"cluster.example.com",
"cluster.local.example.com",
},
},
{
desc: "login with host and port",
Expand Down Expand Up @@ -346,8 +330,8 @@ func TestRun(t *testing.T) {
VerificationURIComplete: "https://cluster.example.com/with/auth/code",
},
},
tokenStore: &fakeStore{
storeErr: errors.New("token_store_fail"),
tokenStore: &oauthtoken.FakeLoadStorer{
StoreErr: errors.New("token_store_fail"),
},
wantCode: autherr.CodeTokenStoreFailure,
wantErr: "token_store_fail",
Expand All @@ -367,30 +351,30 @@ func TestRun(t *testing.T) {
{
desc: "export when token not found",
args: []string{"export", "https://cluster.example.com"},
tokenStore: &fakeStore{
loadToken: nil,
loadErr: autherr.ReauthRequired("https://cluster.example.com"),
tokenStore: &oauthtoken.FakeLoadStorer{
LoadErr: autherr.ReauthRequired("https://cluster.example.com"),
},
wantCode: autherr.CodeReauthRequired,
wantErr: "expired credentials for cluster",
},
{
desc: "export when token store fails",
args: []string{"export", "https://cluster.example.com"},
tokenStore: &fakeStore{
loadToken: nil,
loadErr: fmt.Errorf("token_load_error"),
tokenStore: &oauthtoken.FakeLoadStorer{
LoadErr: fmt.Errorf("token_load_error"),
},
wantCode: autherr.CodeTokenStoreFailure,
wantErr: "token_load_error",
},
{
desc: "export when token expired",
args: []string{"export", "https://cluster.example.com"},
tokenStore: &fakeStore{
loadToken: &oauth2.Token{
AccessToken: "access_token",
Expiry: time.Date(2024, 1, 2, 3, 4, 5, 6, time.UTC),
tokenStore: &oauthtoken.FakeLoadStorer{
Tokens: map[string]*oauth2.Token{
"cluster.example.com": {
AccessToken: "access_token",
Expiry: time.Date(2024, 1, 2, 3, 4, 5, 6, time.UTC),
},
},
},
wantCode: autherr.CodeReauthRequired,
Expand All @@ -399,12 +383,13 @@ func TestRun(t *testing.T) {
{
desc: "export token",
args: []string{"export", "https://cluster.example.com"},
tokenStore: &fakeStore{
loadToken: &oauth2.Token{
AccessToken: "token_data",
Expiry: expiresInFuture,
tokenStore: &oauthtoken.FakeLoadStorer{
Tokens: map[string]*oauth2.Token{
"cluster.example.com": {
AccessToken: "token_data",
Expiry: expiresInFuture,
},
},
loadErr: nil,
},
wantStdoutContaining: []string{
`{"token":{"access_token":"token_data",`, // Should have top-level token element
Expand All @@ -415,12 +400,13 @@ func TestRun(t *testing.T) {
{
desc: "export token with alias",
args: []string{"export", "--alias", "cluster.local.example.com:8080", "https://cluster.example.com"},
tokenStore: &fakeStore{
loadToken: &oauth2.Token{
AccessToken: "token_data",
Expiry: expiresInFuture,
tokenStore: &oauthtoken.FakeLoadStorer{
Tokens: map[string]*oauth2.Token{
"cluster.example.com": {
AccessToken: "token_data",
Expiry: expiresInFuture,
},
},
loadErr: nil,
},
wantStdoutContaining: []string{
`{"token":{"access_token":"token_data",`, // Should have top-level token element
Expand Down Expand Up @@ -448,7 +434,7 @@ func TestRun(t *testing.T) {
root.authenticator = &fakeAuth{}
}
if root.tokenStore == nil {
root.tokenStore = &fakeStore{}
root.tokenStore = oauthtoken.NewFakeLoadStorer()
}

app := makeApp(root)
Expand Down Expand Up @@ -487,8 +473,8 @@ func TestRun(t *testing.T) {
t.Logf("\n====== BEGIN APP STDERR ======\n%s\n====== END APP STDERR ======\n\n", stderr.String())
}
}
if tokenStore, ok := tc.tokenStore.(*fakeStore); ok {
assert.Subset(t, tokenStore.storeClusters, tc.wantStoreCallsFor)
if tokenStore, ok := tc.tokenStore.(*oauthtoken.FakeLoadStorer); ok {
assert.Subset(t, tokenStore.Tokens, tc.wantStored)
}
})
}
Expand Down
3 changes: 1 addition & 2 deletions internal/oauthtoken/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go_library(
srcs = [
"cache_alert.go",
"debug.go",
"file.go",
"fake.go",
"keyring.go",
"load_storer.go",
],
Expand All @@ -23,7 +23,6 @@ go_test(
name = "oauthtoken_test",
srcs = [
"cache_alert_test.go",
"file_test.go",
"keyring_test.go",
],
embed = [":oauthtoken"],
Expand Down
8 changes: 1 addition & 7 deletions internal/oauthtoken/cache_alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package oauthtoken
import (
"bytes"
"context"
"path/filepath"
"testing"
"time"

Expand Down Expand Up @@ -47,13 +46,8 @@ func mustTokenForSubject(t *testing.T, name string) string {

func TestTokenCacheWarning(t *testing.T) {
ctx := context.Background()
configDir := t.TempDir()
testPath := filepath.Join(configDir, "token_store.json")
var testStderr bytes.Buffer
tokenStore := &CacheAlert{
impl: &File{path: testPath},
stderr: &testStderr,
}
tokenStore := NewCacheAlert(NewFakeLoadStorer(), &testStderr)

testTokenAlice := &oauth2.Token{
AccessToken: mustTokenForSubject(t, "alice"),
Expand Down
56 changes: 56 additions & 0 deletions internal/oauthtoken/fake.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's worth naming this fake_test.go so it ends up effectively testonly?

Alternatively, if we want this in the binary - it could be named InMemoryStore or similar, since it seems like its a legitimate implementation

Side question on naming - I've always struggled with Go interface vs. struct naming convention (suffixing all interfaces with -er is sometimes limiting/awkward, avoiding -er for struct types is also sometimes limiting, so I've historically follow the conventions sporadically depending on my mood). I notice here you've chosen FakeLoadStorer for the struct type name - what's your approach to naming structs/interfaces?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's worth naming this fake_test.go so it ends up effectively testonly?

Alternatively, if we want this in the binary - it could be named InMemoryStore or similar, since it seems like its a legitimate implementation

I would if this were a single package, but this is needed in cmd/engflow_auth/main_test.go, so it needs to be exported from the library part of the package. I think that's okay: libraries sometimes provide fake implementations for other folks to test against. keyring itself does this.

Side question on naming - I've always struggled with Go interface vs. struct naming convention (suffixing all interfaces with -er is sometimes limiting/awkward, avoiding -er for struct types is also sometimes limiting, so I've historically follow the conventions sporadically depending on my mood). I notice here you've chosen FakeLoadStorer for the struct type name - what's your approach to naming structs/interfaces?

Renamed to FakeTokenStore. Actually, I think LoadStorer should be renamed to TokenStore, though I haven't done so in this PR. Let me know if I should.

I think the advice to name interfaces after verbs only works if the interface has one or two methods, and it's really generic, like io.ReadCloser. An io.ReadCloser could be basically anything, so it's hard to come up with a more descriptive name.

If something has a specific purpose, it's better to name it after that. For example, io/fs.FS has one method, Open(string) (File, error). FS is a much better name than Opener, and File is a much better name than StatReadCloser.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think LoadStorer should be renamed to TokenStore

Agree 100% - I avoided naming it this since it didn't end in er, and I was making an extra effort not to deviate from convention. Makes sense about the fs.FS naming - I guess the convention wasn't as strong as I had figured

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2024 EngFlow Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oauthtoken

import (
"context"
"fmt"

"golang.org/x/oauth2"
)

// FakeLoadStorer is a test implementation of LoadStorer that stores tokens in
// memory instead of the system keychain.
type FakeLoadStorer struct {
Tokens map[string]*oauth2.Token
LoadErr, StoreErr error
}

var _ LoadStorer = (*FakeLoadStorer)(nil)

func NewFakeLoadStorer() *FakeLoadStorer {
return &FakeLoadStorer{
Tokens: make(map[string]*oauth2.Token),
}
}

func (f *FakeLoadStorer) Load(ctx context.Context, cluster string) (*oauth2.Token, error) {
if f.LoadErr != nil {
return nil, f.LoadErr
}
token, ok := f.Tokens[cluster]
if !ok {
return nil, fmt.Errorf("%s: token not found", cluster)
}
return token, nil
}

func (f *FakeLoadStorer) Store(ctx context.Context, cluster string, token *oauth2.Token) error {
if f.StoreErr != nil {
return f.StoreErr
}
f.Tokens[cluster] = token
return nil
}
Loading