diff --git a/cmd/tick/main.go b/cmd/tick/main.go index 50262b9..5bd0b01 100644 --- a/cmd/tick/main.go +++ b/cmd/tick/main.go @@ -42,6 +42,23 @@ func resolveRegion() (string, error) { return cfg.Region, nil } +func buildAuthService(region string, streams cli.Streams) (auth.Service, error) { + endpoints, err := endpoint.ForRegion(region) + if err != nil { + return auth.Service{}, err + } + return auth.Service{ + AuthorizeURL: endpoints.AuthorizeURL, + Exchanger: auth.Exchanger{ + TokenURL: endpoints.TokenURL, + }, + Store: auth.FileStore{}, + Browser: browserOpener{}, + In: streams.In, + Out: streams.Out, + }, nil +} + func buildRuntime(streams cli.Streams) (*config.Store, auth.Service, *ticktick.Client, error) { store, cfg, err := loadConfigStore() if err != nil { @@ -76,13 +93,15 @@ func main() { Streams: streams, RegionResolver: resolveRegion, LoginAuthResolver: func() (*app.AuthApp, error) { - store, service, _, err := buildRuntime(streams) + store, _, err := loadConfigStore() if err != nil { return nil, err } return &app.AuthApp{ ConfigStore: store, - Service: service, + ServiceForRegion: func(region string) (app.AuthService, error) { + return buildAuthService(region, streams) + }, }, nil }, AuthServiceResolver: func() (app.AuthService, error) { diff --git a/internal/app/auth.go b/internal/app/auth.go index 1b9fc21..8e5bc96 100644 --- a/internal/app/auth.go +++ b/internal/app/auth.go @@ -14,9 +14,12 @@ type AuthService interface { Logout(context.Context) error } +type AuthServiceFactory func(region string) (AuthService, error) + type AuthApp struct { - ConfigStore *config.Store - Service AuthService + ConfigStore *config.Store + Service AuthService + ServiceForRegion AuthServiceFactory } type LoginInput struct { @@ -58,6 +61,17 @@ func (a AuthApp) Login(ctx context.Context, in LoginInput) error { if in.ClientID == "" || in.ClientSecret == "" || in.RedirectURL == "" { return errors.New("login requires client-id, client-secret, and redirect-url") } + service := a.Service + if a.ServiceForRegion != nil { + var err error + service, err = a.ServiceForRegion(in.Region) + if err != nil { + return err + } + } + if service == nil { + return errors.New("auth service is unavailable") + } if a.ConfigStore != nil { cfg.ClientID = in.ClientID cfg.ClientSecret = in.ClientSecret @@ -67,7 +81,7 @@ func (a AuthApp) Login(ctx context.Context, in LoginInput) error { return err } } - _, err := a.Service.Login(ctx, auth.LoginInput{ + _, err := service.Login(ctx, auth.LoginInput{ ClientID: in.ClientID, ClientSecret: in.ClientSecret, RedirectURL: in.RedirectURL, diff --git a/internal/app/auth_test.go b/internal/app/auth_test.go index ecbf13e..b1d07b6 100644 --- a/internal/app/auth_test.go +++ b/internal/app/auth_test.go @@ -36,6 +36,23 @@ func (f fakeAuthService) Logout(context.Context) error { return nil } +type recordingLoginAuthService struct { + loginInput auth.LoginInput +} + +func (r *recordingLoginAuthService) Login(_ context.Context, in auth.LoginInput) (auth.Token, error) { + r.loginInput = in + return auth.Token{AccessToken: "access-1"}, nil +} + +func (r *recordingLoginAuthService) Status(context.Context) (auth.Status, error) { + return auth.Status{}, nil +} + +func (r *recordingLoginAuthService) Logout(context.Context) error { + return nil +} + func TestAuthAppStatus(t *testing.T) { store := config.NewStore(t.TempDir() + "/config.yaml") app := AuthApp{ @@ -125,6 +142,49 @@ func TestAuthAppLoginPersistsConfigOnSuccess(t *testing.T) { } } +func TestAuthAppLoginBuildsServiceForSelectedRegion(t *testing.T) { + store := config.NewStore(t.TempDir() + "/config.yaml") + if err := store.Save(config.Config{ + Region: "ticktick", + ClientID: "old-client-id", + ClientSecret: "old-secret", + RedirectURL: "http://localhost:14573/callback", + }); err != nil { + t.Fatalf("Save() error = %v", err) + } + + selectedRegion := "" + service := &recordingLoginAuthService{} + app := AuthApp{ + ConfigStore: store, + ServiceForRegion: func(region string) (AuthService, error) { + selectedRegion = region + return service, nil + }, + } + + if err := app.Login(context.Background(), LoginInput{ + Region: "dida365", + ClientID: "new-client-id", + ClientSecret: "new-secret", + }); err != nil { + t.Fatalf("Login() error = %v", err) + } + if selectedRegion != "dida365" { + t.Fatalf("service region = %q, want dida365", selectedRegion) + } + if service.loginInput.ClientID != "new-client-id" { + t.Fatalf("ClientID = %q, want new-client-id", service.loginInput.ClientID) + } + cfg, err := store.Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.Region != "dida365" { + t.Fatalf("stored Region = %q, want dida365", cfg.Region) + } +} + func TestAuthAppLoginSucceedsWithoutConfigStoreWhenInputsAreExplicit(t *testing.T) { app := AuthApp{ Service: fakeAuthService{},