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

[2/4] public library: Implement registration and status/keepalive #279

Merged
merged 3 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 41 additions & 0 deletions cmd/public-api-test/main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package main

import (
"bufio"
"errors"
"fmt"
"os"

"github.com/SUSE/connect-ng/pkg/connection"
"github.com/SUSE/connect-ng/pkg/registration"
)

const (
Expand All @@ -15,6 +18,15 @@ func bold(format string, args ...interface{}) {
fmt.Printf("\033[1m"+format+"\033[0m", args...)
}

func waitForUser(message string) {
if os.Getenv("NON_INTERACTIVE") != "true" {
bold("\n%s. Enter to continue\n", message)
bufio.NewReader(os.Stdin).ReadBytes('\n')
} else {
bold("\n%s", message)
}
}

func runDemo(regcode string) error {
opts := connection.DefaultOptions("public-api-demo", "1.0", "DE")

Expand All @@ -39,6 +51,35 @@ func runDemo(regcode string) error {
fmt.Printf("!! len(payload): %d characters\n", len(payload))
fmt.Printf("!! first 40 characters: %s\n", string(payload[0:40]))

bold("2) Registering a client to SCC with a registration code\n")
id, regErr := registration.Register(conn, regcode, hostname, nil)
if regErr != nil {
return regErr
}
bold("!! check https://scc.suse.com/systems/%d\n", id)
fmt.Println("Note: Unless you activate something on the system we do NOT it in WEB UI")
waitForUser("Registration complete")

bold("3) System status // Ping\n")
systemInformation := map[string]any{
"uname": "public api demo - ping",
}

status, statusErr := registration.Status(conn, hostname, systemInformation)
if statusErr != nil {
return statusErr
}

if status != registration.Registered {
return errors.New("Could not finalize registration!")
}
waitForUser("System update complete")

bold("4) Deregistration of the client\n")
if err := registration.Deregister(conn); err != nil {
return err
}
bold("-- System deregistered")
return nil
}

Expand Down
68 changes: 68 additions & 0 deletions pkg/registration/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package registration_test

import (
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func fixture(t *testing.T, path string) []byte {
t.Helper()

absolut, pathErr := filepath.Abs(filepath.Join("../../testdata", path))
if pathErr != nil {
t.Fatalf("Could not build fixture path from %s", path)
}

data, err := os.ReadFile(absolut)
if err != nil {
t.Fatalf("Could not read fixture: %s", err)
}
return data

}

func matchBody(t *testing.T, body string) func(mock.Arguments) {
assert := assert.New(t)

return func(args mock.Arguments) {
request := args.Get(0).(*http.Request)
body, readErr := io.ReadAll(request.Body)

assert.NoError(readErr)
assert.Equal(strings.TrimSpace(string(body)), string(body), "request.Body matches")
}
}

func checkAuthByRegcode(t *testing.T, regcode string) func(mock.Arguments) {
assert := assert.New(t)

return func(args mock.Arguments) {
request := args.Get(0).(*http.Request)
token := request.Header.Get("Authorization")

expected := fmt.Sprintf("Token token=%s", regcode)
assert.Equal(expected, token, "regcode is set as authorization header")
}
}

func checkAuthBySystemCredentials(t *testing.T, login, password string) func(mock.Arguments) {
assert := assert.New(t)
encoded := base64.StdEncoding.EncodeToString([]byte(login + ":" + password))

return func(args mock.Arguments) {
request := args.Get(0).(*http.Request)
token := request.Header.Get("Authorization")

expected := fmt.Sprintf("Basic %s", encoded)
assert.Equal(expected, token, "system credentials are set as authorization header")
}
}
56 changes: 56 additions & 0 deletions pkg/registration/mock_connection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package registration_test

import (
"net/http"

"github.com/SUSE/connect-ng/pkg/connection"
"github.com/stretchr/testify/mock"
)

func mockConnectionWithCredentials() (*mockConnection, *mockCredentials) {
creds := &mockCredentials{}
conn := newMockConnection(creds, "testing")

creds.On("Token").Return("sample-token", nil)
creds.On("Login").Return("sample-login", "sample-password", nil)

creds.On("UpdateToken", mock.Anything).Return(nil)

conn.On("GetCredentials").Return(creds)

return conn, creds
}

func newMockConnection(creds connection.Credentials, hostname string) *mockConnection {
opts := connection.DefaultOptions("testing", "---", "---")
opts.URL = "http://local-testing/"

conn := connection.New(opts, creds)

return &mockConnection{
real: conn,
}
}

type mockConnection struct {
mock.Mock
real connection.Connection
}

func (m *mockConnection) BuildRequest(verb, path string, body any) (*http.Request, error) {

request, err := m.real.BuildRequest(verb, path, body)
return request, err
}

func (m *mockConnection) Do(request *http.Request) ([]byte, error) {
args := m.Called(request)

return args.Get(0).([]byte), args.Error(1)
}

func (m *mockConnection) GetCredentials() connection.Credentials {
args := m.Called()

return args.Get(0).(connection.Credentials)
}
32 changes: 32 additions & 0 deletions pkg/registration/mock_credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package registration_test

import "github.com/stretchr/testify/mock"

type mockCredentials struct {
mock.Mock
}

func (m *mockCredentials) HasAuthentication() bool {
args := m.Called()
return args.Bool(0)
}

func (m *mockCredentials) Login() (string, string, error) {
args := m.Called()
return args.String(0), args.String(1), args.Error(2)
}

func (m *mockCredentials) SetLogin(login, password string) error {
args := m.Called(login, password)
return args.Error(0)
}

func (m *mockCredentials) Token() (string, error) {
args := m.Called()
return args.String(0), args.Error(1)
}

func (m *mockCredentials) UpdateToken(token string) error {
args := m.Called(token)
return args.Error(0)
}
63 changes: 59 additions & 4 deletions pkg/registration/register.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,73 @@
package registration

import (
"github.com/SUSE/connect-ng/internal/collectors"
"encoding/json"

"github.com/SUSE/connect-ng/pkg/connection"
)

type announceRequest struct {
Hostname string `json:"hostname"`
SystemInformation any `json:"hwinfo"`
}

type announceResponse struct {
Id int `json:"id"`
Login string `json:"login"`
Password string `json:"password"`
}

// Register a system by using the given regcode. You also need to provide the
// hostname of the system plus extra info that is to be bundled when registering
// a system.
func Register(conn connection.Connection, regcode, hostname string, info collectors.Result) error {
return nil
func Register(conn connection.Connection, regcode, hostname string, systemInformation any) (int, error) {
reg := announceResponse{}
payload := announceRequest{
Hostname: hostname,
SystemInformation: systemInformation,
}
creds := conn.GetCredentials()

request, buildErr := conn.BuildRequest("POST", "/connect/subscriptions/systems", payload)
if buildErr != nil {
return 0, buildErr
}

connection.AddRegcodeAuth(request, regcode)

response, doErr := conn.Do(request)
if doErr != nil {
return 0, doErr
}

if err := json.Unmarshal(response, &reg); err != nil {
return 0, err
}

credErr := creds.SetLogin(reg.Login, reg.Password)

return reg.Id, credErr
}

// De-register the system pointed by the given authorized connection.
func Deregister(conn connection.Connection) error {
return nil
creds := conn.GetCredentials()
request, buildErr := conn.BuildRequest("DELETE", "/connect/systems", nil)
if buildErr != nil {
return buildErr
}

login, password, credErr := creds.Login()
if credErr != nil {
return credErr
}

connection.AddSystemAuth(request, login, password)

_, doErr := conn.Do(request)
if doErr != nil {
return doErr
}

return creds.SetLogin("", "")
}
70 changes: 70 additions & 0 deletions pkg/registration/register_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package registration_test

import (
"errors"
"testing"

"github.com/SUSE/connect-ng/pkg/registration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestRegisterSuccess(t *testing.T) {
assert := assert.New(t)

conn, creds := mockConnectionWithCredentials()

// 204 No Content
payload := fixture(t, "pkg/registration/announce_success.json")

conn.On("Do", mock.Anything).Return(payload, nil).Run(checkAuthByRegcode(t, "regcode"))
creds.On("SetLogin", "SCC_login", "sample-password").Return(nil)

_, err := registration.Register(conn, "regcode", "hostname", nil)
assert.NoError(err)

conn.AssertExpectations(t)
}

func TestRegisterFailed(t *testing.T) {
assert := assert.New(t)

conn, _ := mockConnectionWithCredentials()

// 404 Not found / announce failed
conn.On("Do", mock.Anything).Return([]byte{}, errors.New("Invalid registration token supplied"))

_, err := registration.Register(conn, "regcode", "hostname", nil)
assert.ErrorContains(err, "Invalid registration token")

conn.AssertExpectations(t)
}

func TestDeRegisterSuccess(t *testing.T) {
assert := assert.New(t)

conn, creds := mockConnectionWithCredentials()

// 404 Not found / announce failed
conn.On("Do", mock.Anything).Return([]byte{}, nil)
creds.On("SetLogin", "", "").Return(nil)

err := registration.Deregister(conn)
assert.NoError(err)

conn.AssertExpectations(t)
}

func TestDeRegisterInvalid(t *testing.T) {
assert := assert.New(t)

conn, _ := mockConnectionWithCredentials()

// 404 Not found / announce failed
conn.On("Do", mock.Anything).Return([]byte{}, errors.New("Not found"))

err := registration.Deregister(conn)
assert.Error(err)

conn.AssertExpectations(t)
}
Loading
Loading