Skip to content

Commit

Permalink
Implement registration and status/keepalive in the public library
Browse files Browse the repository at this point in the history
The registration creates a new system in SCC and allows for performing
the status/keepalive call to update the provided systemInformation.

Note: Instead of historically named hwinfo, here everything is named
systemInformation
  • Loading branch information
felixsch committed Jan 2, 2025
1 parent 98e1c60 commit a6f08bb
Show file tree
Hide file tree
Showing 10 changed files with 422 additions and 7 deletions.
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 @@ -40,6 +52,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]string{
"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) (int, []byte, error) {
args := m.Called(request)

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

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.AuthByRegcode(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.AuthBySystemCredentials(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(204, 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(404, []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(204, []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(404, []byte{}, errors.New("Not found"))

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

conn.AssertExpectations(t)
}
Loading

0 comments on commit a6f08bb

Please sign in to comment.