Skip to content
Open
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
183 changes: 183 additions & 0 deletions internal/integration_tests/applications_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package integration_tests

import (
"context"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/authorizerdev/authorizer/internal/graph/model"
"github.com/authorizerdev/authorizer/internal/storage/schemas"
)

// TestApplications tests the M2M Application CRUD operations at the storage layer
func TestApplications(t *testing.T) {
cfg := getTestConfig()
ts := initTestSetup(t, cfg)

// Use background context for storage calls
storageCtx := context.Background()

t.Run("should create application", func(t *testing.T) {
application := &schemas.Application{
Name: "test-m2m-app-" + uuid.New().String(),
Description: "Test M2M application",
ClientID: uuid.New().String(),
ClientSecret: "test-secret",
Scopes: "read write",
Roles: "admin",
IsActive: true,
CreatedBy: uuid.New().String(),
}

err := ts.StorageProvider.CreateApplication(storageCtx, application)
require.NoError(t, err)
assert.NotEmpty(t, application.ID)
assert.NotZero(t, application.CreatedAt)
assert.NotZero(t, application.UpdatedAt)
})

t.Run("should get application by ID", func(t *testing.T) {
application := &schemas.Application{
Name: "test-m2m-app-by-id-" + uuid.New().String(),
Description: "Test M2M application get by ID",
ClientID: uuid.New().String(),
ClientSecret: "test-secret",
Scopes: "read",
Roles: "user",
IsActive: true,
CreatedBy: uuid.New().String(),
}

err := ts.StorageProvider.CreateApplication(storageCtx, application)
require.NoError(t, err)
require.NotEmpty(t, application.ID)

retrieved, err := ts.StorageProvider.GetApplicationByID(storageCtx, application.ID)
require.NoError(t, err)
assert.NotNil(t, retrieved)
assert.Equal(t, application.Name, retrieved.Name)
assert.Equal(t, application.ClientID, retrieved.ClientID)
assert.Equal(t, application.Description, retrieved.Description)
assert.Equal(t, application.IsActive, retrieved.IsActive)
})

t.Run("should fail to get application with non-existent ID", func(t *testing.T) {
retrieved, err := ts.StorageProvider.GetApplicationByID(storageCtx, uuid.New().String())
assert.Error(t, err)
assert.Nil(t, retrieved)
})

t.Run("should get application by client ID", func(t *testing.T) {
clientID := uuid.New().String()
application := &schemas.Application{
Name: "test-m2m-app-by-clientid-" + uuid.New().String(),
Description: "Test M2M application get by client ID",
ClientID: clientID,
ClientSecret: "test-secret",
Scopes: "read write",
Roles: "user",
IsActive: true,
CreatedBy: uuid.New().String(),
}

err := ts.StorageProvider.CreateApplication(storageCtx, application)
require.NoError(t, err)

retrieved, err := ts.StorageProvider.GetApplicationByClientID(storageCtx, clientID)
require.NoError(t, err)
assert.NotNil(t, retrieved)
assert.Equal(t, clientID, retrieved.ClientID)
assert.Equal(t, application.Name, retrieved.Name)
})

t.Run("should fail to get application with non-existent client ID", func(t *testing.T) {
retrieved, err := ts.StorageProvider.GetApplicationByClientID(storageCtx, uuid.New().String())
assert.Error(t, err)
assert.Nil(t, retrieved)
})

t.Run("should list applications with pagination", func(t *testing.T) {
// Create two applications to ensure list returns results
for i := 0; i < 2; i++ {
app := &schemas.Application{
Name: "test-m2m-app-list-" + uuid.New().String(),
Description: "Test M2M application for list",
ClientID: uuid.New().String(),
ClientSecret: "test-secret",
Scopes: "read",
Roles: "user",
IsActive: true,
CreatedBy: uuid.New().String(),
}
err := ts.StorageProvider.CreateApplication(storageCtx, app)
require.NoError(t, err)
}

pagination := &model.Pagination{
Limit: 10,
Offset: 0,
}
applications, paginationResult, err := ts.StorageProvider.ListApplications(storageCtx, pagination)
require.NoError(t, err)
assert.NotNil(t, paginationResult)
assert.GreaterOrEqual(t, len(applications), 2)
assert.GreaterOrEqual(t, paginationResult.Total, int64(2))
})

t.Run("should update application", func(t *testing.T) {
application := &schemas.Application{
Name: "test-m2m-app-update-" + uuid.New().String(),
Description: "Test M2M application before update",
ClientID: uuid.New().String(),
ClientSecret: "test-secret",
Scopes: "read",
Roles: "user",
IsActive: true,
CreatedBy: uuid.New().String(),
}

err := ts.StorageProvider.CreateApplication(storageCtx, application)
require.NoError(t, err)
require.NotEmpty(t, application.ID)

application.Description = "Test M2M application after update"
application.Scopes = "read write"
application.IsActive = false

err = ts.StorageProvider.UpdateApplication(storageCtx, application)
require.NoError(t, err)

retrieved, err := ts.StorageProvider.GetApplicationByID(storageCtx, application.ID)
require.NoError(t, err)
assert.Equal(t, "Test M2M application after update", retrieved.Description)
assert.Equal(t, "read write", retrieved.Scopes)
assert.False(t, retrieved.IsActive)
})

t.Run("should delete application", func(t *testing.T) {
application := &schemas.Application{
Name: "test-m2m-app-delete-" + uuid.New().String(),
Description: "Test M2M application for deletion",
ClientID: uuid.New().String(),
ClientSecret: "test-secret",
Scopes: "read",
Roles: "user",
IsActive: true,
CreatedBy: uuid.New().String(),
}

err := ts.StorageProvider.CreateApplication(storageCtx, application)
require.NoError(t, err)
require.NotEmpty(t, application.ID)

err = ts.StorageProvider.DeleteApplication(storageCtx, application.ID)
require.NoError(t, err)

retrieved, err := ts.StorageProvider.GetApplicationByID(storageCtx, application.ID)
assert.Error(t, err)
assert.Nil(t, retrieved)
})
}
139 changes: 139 additions & 0 deletions internal/storage/db/arangodb/application.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package arangodb

import (
"context"
"fmt"
"time"

arangoDriver "github.com/arangodb/go-driver"
"github.com/google/uuid"

"github.com/authorizerdev/authorizer/internal/graph/model"
"github.com/authorizerdev/authorizer/internal/storage/schemas"
)

// CreateApplication creates a new M2M application
func (p *provider) CreateApplication(ctx context.Context, application *schemas.Application) error {
if application.ID == "" {
application.ID = uuid.New().String()
}
application.Key = application.ID
application.CreatedAt = time.Now().Unix()
application.UpdatedAt = time.Now().Unix()
applicationCollection, _ := p.db.Collection(ctx, schemas.Collections.Application)
meta, err := applicationCollection.CreateDocument(ctx, application)
if err != nil {
return err
}
application.Key = meta.Key
application.ID = meta.ID.String()
return nil
}

// GetApplicationByID retrieves an application by ID
func (p *provider) GetApplicationByID(ctx context.Context, id string) (*schemas.Application, error) {
var application schemas.Application
query := fmt.Sprintf("FOR d in %s FILTER d._id == @id RETURN d", schemas.Collections.Application)
bindVars := map[string]interface{}{
"id": id,
}
cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil {
return nil, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if application.ID == "" {
return nil, fmt.Errorf("application not found")
}
break
}
_, err := cursor.ReadDocument(ctx, &application)
if err != nil {
return nil, err
}
}
return &application, nil
}

// GetApplicationByClientID retrieves an application by client ID
func (p *provider) GetApplicationByClientID(ctx context.Context, clientID string) (*schemas.Application, error) {
var application schemas.Application
query := fmt.Sprintf("FOR d in %s FILTER d.client_id == @client_id RETURN d", schemas.Collections.Application)
bindVars := map[string]interface{}{
"client_id": clientID,
}
cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil {
return nil, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if application.ID == "" {
return nil, fmt.Errorf("application not found")
}
break
}
_, err := cursor.ReadDocument(ctx, &application)
if err != nil {
return nil, err
}
}
return &application, nil
}

// ListApplications lists all applications with pagination
func (p *provider) ListApplications(ctx context.Context, pagination *model.Pagination) ([]*schemas.Application, *model.Pagination, error) {
applications := []*schemas.Application{}
query := fmt.Sprintf("FOR d in %s SORT d.created_at DESC LIMIT %d, %d RETURN d", schemas.Collections.Application, pagination.Offset, pagination.Limit)
sctx := arangoDriver.WithQueryFullCount(ctx)
cursor, err := p.db.Query(sctx, query, nil)
if err != nil {
return nil, nil, err
}
defer cursor.Close()
paginationClone := pagination
paginationClone.Total = cursor.Statistics().FullCount()
for {
var application schemas.Application
meta, err := cursor.ReadDocument(ctx, &application)
if arangoDriver.IsNoMoreDocuments(err) {
break
} else if err != nil {
return nil, nil, err
}
if meta.Key != "" {
applications = append(applications, &application)
}
}
return applications, paginationClone, nil
}

// UpdateApplication updates an application
func (p *provider) UpdateApplication(ctx context.Context, application *schemas.Application) error {
application.UpdatedAt = time.Now().Unix()
applicationCollection, _ := p.db.Collection(ctx, schemas.Collections.Application)
meta, err := applicationCollection.UpdateDocument(ctx, application.Key, application)
if err != nil {
return err
}
application.Key = meta.Key
application.ID = meta.ID.String()
return nil
}

// DeleteApplication deletes an application by ID
func (p *provider) DeleteApplication(ctx context.Context, id string) error {
application, err := p.GetApplicationByID(ctx, id)
if err != nil {
return err
}
applicationCollection, _ := p.db.Collection(ctx, schemas.Collections.Application)
_, err = applicationCollection.RemoveDocument(ctx, application.Key)
if err != nil {
return err
}
return nil
}
24 changes: 24 additions & 0 deletions internal/storage/db/arangodb/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,30 @@ func NewProvider(cfg *config.Config, deps *Dependencies) (*provider, error) {
Sparse: true,
})

// Application collection and indexes
applicationCollectionExists, err := arangodb.CollectionExists(ctx, schemas.Collections.Application)
if err != nil {
return nil, err
}
if !applicationCollectionExists {
_, err = arangodb.CreateCollection(ctx, schemas.Collections.Application, nil)
if err != nil {
return nil, err
}
}
applicationCollection, err := arangodb.Collection(ctx, schemas.Collections.Application)
if err != nil {
return nil, err
}
applicationCollection.EnsureHashIndex(ctx, []string{"name"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
applicationCollection.EnsureHashIndex(ctx, []string{"client_id"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})

return &provider{
config: cfg,
dependencies: deps,
Expand Down
Loading