mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-20 20:06:33 +00:00
137 lines
4.3 KiB
Go
137 lines
4.3 KiB
Go
package scimsync
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/apperror"
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestServiceProviderOperationsReturnSpecificNotFoundErrors(t *testing.T) {
|
|
service := newService(testutils.NewDatabaseForTest(t), nil)
|
|
|
|
_, err := service.CreateServiceProvider(t.Context(), &ScimServiceProviderCreateDTO{
|
|
Endpoint: "https://scim.example.com",
|
|
OidcClientID: "missing-client",
|
|
})
|
|
require.True(t, apperror.IsCode(err, apperror.CodeNotFound))
|
|
|
|
_, err = service.GetServiceProvider(t.Context(), "missing-provider")
|
|
require.True(t, apperror.IsCode(err, apperror.CodeNotFound))
|
|
|
|
err = service.DeleteServiceProvider(t.Context(), "missing-provider")
|
|
require.True(t, apperror.IsCode(err, apperror.CodeNotFound))
|
|
}
|
|
|
|
func TestServiceProviderCreateAndUpdate(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newService(db, nil)
|
|
|
|
// Create two clients so provider creation and reassignment both satisfy the foreign key
|
|
require.NoError(t, db.Create(&[]model.OidcClient{
|
|
{Base: model.Base{ID: "client-1"}, Name: "Client 1"},
|
|
{Base: model.Base{ID: "client-2"}, Name: "Client 2"},
|
|
}).Error)
|
|
|
|
// Create the provider with its initial client in one transaction
|
|
provider, err := service.CreateServiceProvider(t.Context(), &ScimServiceProviderCreateDTO{
|
|
Endpoint: "https://scim.example.com/v1",
|
|
OidcClientID: "client-1",
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, provider.ID)
|
|
|
|
// Move the provider to the second client in one transaction
|
|
provider, err = service.UpdateServiceProvider(t.Context(), provider.ID, &ScimServiceProviderCreateDTO{
|
|
Endpoint: "https://scim.example.com/v2",
|
|
OidcClientID: "client-2",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "https://scim.example.com/v2", provider.Endpoint)
|
|
require.Equal(t, "client-2", provider.OidcClientID)
|
|
|
|
// Verify the committed provider retains both updated values
|
|
persisted, err := service.GetServiceProvider(t.Context(), provider.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, provider.Endpoint, persisted.Endpoint)
|
|
require.Equal(t, provider.OidcClientID, persisted.OidcClientID)
|
|
|
|
// Verify SQLite accepts the read-only snapshot used by synchronization
|
|
snapshot, err := service.loadSyncSnapshot(t.Context(), provider.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, provider.ID, snapshot.provider.ID)
|
|
}
|
|
|
|
func TestSyncSnapshotTxOptions(t *testing.T) {
|
|
require.Equal(t, &sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: true}, syncSnapshotTxOptions("postgres"))
|
|
require.Equal(t, &sql.TxOptions{ReadOnly: true}, syncSnapshotTxOptions("sqlite"))
|
|
}
|
|
|
|
func TestSyncServiceProvidersLimitsConcurrencyAndJoinsErrors(t *testing.T) {
|
|
providers := make([]ServiceProvider, 8)
|
|
for i := range providers {
|
|
providers[i].ID = fmt.Sprintf("provider-%d", i)
|
|
}
|
|
|
|
started := make(chan string, len(providers))
|
|
release := make(chan struct{})
|
|
done := make(chan error, 1)
|
|
var active atomic.Int32
|
|
var maximum atomic.Int32
|
|
|
|
go func() {
|
|
done <- syncServiceProviders(t.Context(), providers, func(_ context.Context, providerID string) error {
|
|
current := active.Add(1)
|
|
for {
|
|
previous := maximum.Load()
|
|
if current <= previous || maximum.CompareAndSwap(previous, current) {
|
|
break
|
|
}
|
|
}
|
|
started <- providerID
|
|
<-release
|
|
active.Add(-1)
|
|
|
|
if providerID == "provider-0" || providerID == "provider-7" {
|
|
return errors.New(providerID + " failed")
|
|
}
|
|
return nil
|
|
})
|
|
}()
|
|
|
|
// Keep the first batch blocked so a fifth provider would expose a broken concurrency limit
|
|
firstBatch := make([]string, 0, syncProviderConcurrency)
|
|
|
|
firstBatchLoop:
|
|
for range syncProviderConcurrency {
|
|
select {
|
|
case providerID := <-started:
|
|
firstBatch = append(firstBatch, providerID)
|
|
case <-time.After(2 * time.Second):
|
|
break firstBatchLoop
|
|
}
|
|
}
|
|
|
|
var unexpectedProvider string
|
|
select {
|
|
case unexpectedProvider = <-started:
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
close(release)
|
|
|
|
err := <-done
|
|
require.Len(t, firstBatch, syncProviderConcurrency)
|
|
require.Empty(t, unexpectedProvider)
|
|
require.EqualValues(t, syncProviderConcurrency, maximum.Load())
|
|
require.ErrorContains(t, err, "provider-0 failed")
|
|
require.ErrorContains(t, err, "provider-7 failed")
|
|
}
|