diff --git a/internal/orchestra/login/login.go b/internal/orchestra/login/login.go index 056bfb63..f2a95529 100644 --- a/internal/orchestra/login/login.go +++ b/internal/orchestra/login/login.go @@ -12,16 +12,16 @@ import ( // Config contains configs for logging in with the OONI orchestra. type Config struct { - BaseURL string - ClientID string - HTTPClient *http.Client - Logger log.Logger - Password string - UserAgent string + BaseURL string + Credentials Credentials + HTTPClient *http.Client + Logger log.Logger + UserAgent string } -type request struct { - Username string `json:"username"` +// Credentials contains the login credentials +type Credentials struct { + ClientID string `json:"username"` Password string `json:"password"` } @@ -33,17 +33,13 @@ type Auth struct { // Do logs this probe in with OONI orchestra func Do(ctx context.Context, config Config) (*Auth, error) { - req := &request{ - Password: config.Password, - Username: config.ClientID, - } var resp Auth err := (&jsonapi.Client{ BaseURL: config.BaseURL, HTTPClient: config.HTTPClient, Logger: config.Logger, UserAgent: config.UserAgent, - }).Create(ctx, "/api/v1/login", req, &resp) + }).Create(ctx, "/api/v1/login", config.Credentials, &resp) if err != nil { return nil, err } diff --git a/internal/orchestra/statefile/statefile.go b/internal/orchestra/statefile/statefile.go new file mode 100644 index 00000000..e92530c8 --- /dev/null +++ b/internal/orchestra/statefile/statefile.go @@ -0,0 +1,81 @@ +// Package statefile defines the state file +package statefile + +import ( + "errors" + "sync" + "time" + + "github.com/ooni/probe-engine/internal/orchestra/login" +) + +// State is the state stored inside the state file +type State struct { + ClientID string + Expire time.Time + Password string + Token string +} + +// Auth returns an authentication structure, if possible, otherwise +// it returns nil, meaning that you should login again. +func (s State) Auth() *login.Auth { + if s.Token == "" { + return nil + } + if time.Now().Add(30 * time.Second).After(s.Expire) { + return nil + } + return &login.Auth{ + Expire: s.Expire, + Token: s.Token, + } +} + +// Credentials returns login credentials, if possible, otherwise it +// returns nil, meaning that you should create an account. +func (s State) Credentials() *login.Credentials { + if s.ClientID == "" { + return nil + } + if s.Password == "" { + return nil + } + return &login.Credentials{ + ClientID: s.ClientID, + Password: s.Password, + } +} + +// StateFile is a generic state file +type StateFile interface { + Set(*State) error + Get() (*State, error) +} + +type memory struct { + state State + mu sync.Mutex +} + +// NewMemory creates a new state file in memory +func NewMemory(workdir string) StateFile { + return &memory{} +} + +func (sf *memory) Set(s *State) error { + if s == nil { + return errors.New("passed nil pointer") + } + sf.mu.Lock() + defer sf.mu.Unlock() + sf.state = *s + return nil +} + +func (sf *memory) Get() (*State, error) { + sf.mu.Lock() + defer sf.mu.Unlock() + state := sf.state + return &state, nil +} diff --git a/internal/orchestra/statefile/statefile_test.go b/internal/orchestra/statefile/statefile_test.go new file mode 100644 index 00000000..ddc48767 --- /dev/null +++ b/internal/orchestra/statefile/statefile_test.go @@ -0,0 +1,94 @@ +package statefile + +import ( + "testing" + "time" +) + +func TestUnitStateAuth(t *testing.T) { + t.Run("with no Token", func(t *testing.T) { + state := State{Expire: time.Now().Add(10 * time.Hour)} + if state.Auth() != nil { + t.Fatal("expected nil here") + } + }) + t.Run("with expired Token", func(t *testing.T) { + state := State{ + Expire: time.Now().Add(-1 * time.Hour), + Token: "xx-x-xxx-xx", + } + if state.Auth() != nil { + t.Fatal("expected nil here") + } + }) + t.Run("with good Token", func(t *testing.T) { + state := State{ + Expire: time.Now().Add(10 * time.Hour), + Token: "xx-x-xxx-xx", + } + if state.Auth() == nil { + t.Fatal("expected valid auth here") + } + }) +} + +func TestUnitStateCredentials(t *testing.T) { + t.Run("with no ClientID", func(t *testing.T) { + state := State{} + if state.Credentials() != nil { + t.Fatal("expected nil here") + } + }) + t.Run("with no Password", func(t *testing.T) { + state := State{ + ClientID: "xx-x-xxx-xx", + } + if state.Credentials() != nil { + t.Fatal("expected nil here") + } + }) + t.Run("with all good", func(t *testing.T) { + state := State{ + ClientID: "xx-x-xxx-xx", + Password: "xx", + } + if state.Credentials() == nil { + t.Fatal("expected valid auth here") + } + }) +} + +func TestUnitStateFileMemory(t *testing.T) { + sf := NewMemory("/tmp") + if sf == nil { + t.Fatal("expected non nil pointer here") + } + if err := sf.Set(nil); err == nil { + t.Fatal("expected an error here") + } + s := State{ + Expire: time.Now(), + Password: "xy", + Token: "abc", + ClientID: "xx", + } + if err := sf.Set(&s); err != nil { + t.Fatal(err) + } + os, err := sf.Get() + if err != nil { + t.Fatal(err) + } + if s.ClientID != os.ClientID { + t.Fatal("the ClientID field has changed") + } + if !s.Expire.Equal(os.Expire) { + t.Fatal("the Expire field has changed") + } + if s.Password != os.Password { + t.Fatal("the Password field has changed") + } + if s.Token != os.Token { + t.Fatal("the Token field has changed") + } +} diff --git a/internal/orchestra/testorchestra/testorchestra.go b/internal/orchestra/testorchestra/testorchestra.go index cbb8af3a..7e243d60 100644 --- a/internal/orchestra/testorchestra/testorchestra.go +++ b/internal/orchestra/testorchestra/testorchestra.go @@ -35,11 +35,13 @@ func Register() (string, error) { // information on success, and an error on failure. func Login(clientID string) (*login.Auth, error) { return login.Do(context.Background(), login.Config{ - BaseURL: "https://ps-test.ooni.io", - ClientID: clientID, + BaseURL: "https://ps-test.ooni.io", + Credentials: login.Credentials{ + ClientID: clientID, + Password: password, + }, HTTPClient: http.DefaultClient, Logger: log.Log, - Password: password, UserAgent: "miniooni/0.1.0-dev", }) }