Skip to content

box: added logic for working with Tarantool schema #446

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

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.

### Added

- Implemented all box.schema.user operations requests and sugar interface (#426).
- Implemented box.session.su request and sugar interface only for current session granting (#426).

### Changed

### Fixed
Expand Down
12 changes: 12 additions & 0 deletions box/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,23 @@

// New returns a new instance of the box structure, which implements the Box interface.
func New(conn tarantool.Doer) *Box {
if conn == nil {
// Check if the provided Tarantool connection is nil, and if it is, panic with an error message.

Check failure on line 16 in box/box.go

View workflow job for this annotation

GitHub Actions / golangci-lint

The line is 104 characters long, which exceeds the maximum of 100 characters. (lll)

Check failure on line 16 in box/box.go

View workflow job for this annotation

GitHub Actions / golangci-lint

The line is 104 characters long, which exceeds the maximum of 100 characters. (lll)
// panic early helps to catch and fix nil pointer issues in the code.
panic("tarantool connection cannot be nil")
}

return &Box{
conn: conn, // Assigns the provided Tarantool connection.
}
}

// Schema returns a new Schema instance, providing access to schema-related operations.
// It uses the connection from the Box instance to communicate with Tarantool.
func (b *Box) Schema() *Schema {
return newSchema(b.conn)
}

// Info retrieves the current information of the Tarantool instance.
// It calls the "box.info" function and parses the result into the Info structure.
func (b *Box) Info() (Info, error) {
Expand Down
91 changes: 74 additions & 17 deletions box/box_test.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,86 @@
package box_test

import (
"context"
"errors"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tarantool/go-tarantool/v2/box"
"github.com/tarantool/go-tarantool/v2/test_helpers"
)

func TestNew(t *testing.T) {
// Create a box instance with a nil connection. This should lead to a panic later.
b := box.New(nil)

// Ensure the box instance is not nil (which it shouldn't be), but this is not meaningful
// since we will panic when we call the Info method with the nil connection.
require.NotNil(t, b)

// We expect a panic because we are passing a nil connection (nil Doer) to the By function.
// The library does not control this zone, and the nil connection would cause a runtime error
// when we attempt to call methods (like Info) on it.
// This test ensures that such an invalid state is correctly handled by causing a panic,
// as it's outside the library's responsibility.
require.Panics(t, func() {

// Calling Info on a box with a nil connection will result in a panic, since the underlying
// connection (Doer) cannot perform the requested action (it's nil).
_, _ = b.Info()
t.Parallel()

// Create a box instance with a nil connection. This should lead to a panic.
require.Panics(t, func() { box.New(nil) })
}

func TestMocked(t *testing.T) {
t.Run("Box.Info", func(t *testing.T) {
t.Parallel()

data := []interface{}{
map[string]interface{}{
"version": "1.0.0",
"id": nil,
"ro": false,
"uuid": "uuid",
"pid": 456,
"status": "status",
"lsn": 123,
"replication": nil,
},
}
mock := test_helpers.NewMockDoer(t,
test_helpers.NewMockResponse(t, data),
)
b := box.New(&mock)

info, err := b.Info()
require.NoError(t, err)

assert.Equal(t, "1.0.0", info.Version)
assert.Equal(t, 456, info.PID)
})

t.Run("Box.Schema.User.Info", func(t *testing.T) {
t.Parallel()

data := []interface{}{
[]interface{}{
[]interface{}{"read,write,execute", "universe", ""},
},
}
mock := test_helpers.NewMockDoer(t,
test_helpers.NewMockResponse(t, data),
)
b := box.New(&mock)

privs, err := b.Schema().User().Info(context.Background(), "username")
require.NoError(t, err)

assert.Equal(t, []box.Privilege{
{
Permissions: []box.Permission{box.PermissionRead, box.PermissionWrite, box.PermissionExecute},

Check failure on line 67 in box/box_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

The line is 110 characters long, which exceeds the maximum of 100 characters. (lll)

Check failure on line 67 in box/box_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

The line is 110 characters long, which exceeds the maximum of 100 characters. (lll)
Type: box.PrivilegeUniverse,
Name: "",
},
}, privs)
})

t.Run("Box.Session.Su", func(t *testing.T) {
t.Parallel()

mock := test_helpers.NewMockDoer(t,
test_helpers.NewMockResponse(t, []interface{}{}),
errors.New("user not found or supplied credentials are invalid"),
)
b := box.New(&mock)

err := b.Session().Su(context.Background(), "admin")
require.NoError(t, err)
})
}
189 changes: 186 additions & 3 deletions box/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//
// Terminal 2:
// $ go test -v example_test.go

package box_test

import (
Expand All @@ -18,7 +19,7 @@ import (
"github.com/tarantool/go-tarantool/v2/box"
)

func Example() {
func ExampleBox_Info() {
dialer := tarantool.NetDialer{
Address: "127.0.0.1:3013",
User: "test",
Expand Down Expand Up @@ -55,6 +56,188 @@ func Example() {
log.Fatalf("Box info uuids are not equal")
}

fmt.Printf("Box info uuids are equal")
fmt.Printf("Current box info: %+v\n", resp.Info)
fmt.Printf("Box info uuids are equal\n")
fmt.Printf("Current box ro: %+v", resp.Info.RO)
// Output:
// Box info uuids are equal
// Current box ro: false
}

func ExampleSchemaUser_Exists() {
dialer := tarantool.NetDialer{
Address: "127.0.0.1:3013",
User: "test",
Password: "test",
}
ctx := context.Background()

client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})

if err != nil {
log.Fatalf("Failed to connect: %s", err)
}

// You can use UserExistsRequest type and call it directly.
fut := client.Do(box.NewUserExistsRequest("user"))

resp := &box.UserExistsResponse{}

err = fut.GetTyped(resp)
if err != nil {
log.Fatalf("Failed get box schema user exists with error: %s", err)
}

// Or use simple User implementation.
b := box.New(client)
exists, err := b.Schema().User().Exists(ctx, "user")
if err != nil {
log.Fatalf("Failed get box schema user exists with error: %s", err)
}

if exists != resp.Exists {
log.Fatalf("Box schema users exists are not equal")
}

fmt.Printf("Box schema users exists are equal\n")
fmt.Printf("Current exists state: %+v", exists)
// Output:
// Box schema users exists are equal
// Current exists state: false
}

func ExampleSchemaUser_Create() {
// Connect to Tarantool.
dialer := tarantool.NetDialer{
Address: "127.0.0.1:3013",
User: "test",
Password: "test",
}
ctx := context.Background()

client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
if err != nil {
log.Fatalf("Failed to connect: %s", err)
}

// Create SchemaUser.
schemaUser := box.NewSchemaUser(client)

// Create a new user.
username := "new_user"
options := box.UserCreateOptions{
IfNotExists: true,
Password: "secure_password",
}
err = schemaUser.Create(ctx, username, options)
if err != nil {
log.Fatalf("Failed to create user: %s", err)
}

fmt.Printf("User '%s' created successfully\n", username)
// Output:
// User 'new_user' created successfully
}

func ExampleSchemaUser_Drop() {
// Connect to Tarantool.
dialer := tarantool.NetDialer{
Address: "127.0.0.1:3013",
User: "test",
Password: "test",
}
ctx := context.Background()

client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
if err != nil {
log.Fatalf("Failed to connect: %s", err)
}

// Create SchemaUser.
schemaUser := box.NewSchemaUser(client)

// Drop an existing user.
username := "new_user"
options := box.UserDropOptions{
IfExists: true,
}
err = schemaUser.Drop(ctx, username, options)
if err != nil {
log.Fatalf("Failed to drop user: %s", err)
}

exists, err := schemaUser.Exists(ctx, username)
if err != nil {
log.Fatalf("Failed to get user exists: %s", err)
}

fmt.Printf("User '%s' dropped successfully\n", username)
fmt.Printf("User '%s' exists status: %v \n", username, exists)
// Output:
// User 'new_user' dropped successfully
// User 'new_user' exists status: false
}

func ExampleSchemaUser_Password() {
// Connect to Tarantool.
dialer := tarantool.NetDialer{
Address: "127.0.0.1:3013",
User: "test",
Password: "test",
}
ctx := context.Background()

client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
if err != nil {
log.Fatalf("Failed to connect: %s", err)
}

// Create SchemaUser.
schemaUser := box.NewSchemaUser(client)

// Get the password hash.
password := "my-password"
passwordHash, err := schemaUser.Password(ctx, password)
if err != nil {
log.Fatalf("Failed to get password hash: %s", err)
}

fmt.Printf("Password '%s' hash: %s", password, passwordHash)
// Output:
// Password 'my-password' hash: 3PHNAQGFWFo0KRfToxNgDXHj2i8=
}

func ExampleSchemaUser_Info() {
// Connect to Tarantool.
dialer := tarantool.NetDialer{
Address: "127.0.0.1:3013",
User: "test",
Password: "test",
}
ctx := context.Background()

client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
if err != nil {
log.Fatalf("Failed to connect: %s", err)
}

// Create SchemaUser.
schemaUser := box.NewSchemaUser(client)

info, err := schemaUser.Info(ctx, "test")
if err != nil {
log.Fatalf("Failed to get password hash: %s", err)
}

hasSuper := false
for _, i := range info {
if i.Name == "super" && i.Type == box.PrivilegeRole {
hasSuper = true
}
}

if hasSuper {
fmt.Printf("User have super privileges")
}
// Output:
// User have super privileges
}
17 changes: 6 additions & 11 deletions box/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,25 +105,20 @@ func (ir *InfoResponse) DecodeMsgpack(d *msgpack.Decoder) error {
}

ir.Info = i

return nil
}

// InfoRequest represents a request to retrieve information about the Tarantool instance.
// It implements the tarantool.Request interface.
type InfoRequest struct {
baseRequest
}

// Body method is used to serialize the request's body.
// It is part of the tarantool.Request interface implementation.
func (i InfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error {
return i.impl.Body(res, enc)
*tarantool.CallRequest // Underlying Tarantool call request.
}

// NewInfoRequest returns a new empty info request.
func NewInfoRequest() InfoRequest {
req := InfoRequest{}
req.impl = newCall("box.info")
return req
callReq := tarantool.NewCallRequest("box.info")

return InfoRequest{
callReq,
}
}
Loading
Loading