feat: sync Telegram Login OIDC provider
This commit is contained in:
parent
30774f8c39
commit
ebead9e98c
63 changed files with 11374 additions and 37 deletions
|
|
@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("migrate star gift lifecycle schema: %v", err)
|
||||
}
|
||||
if status.Dirty || status.Empty || status.Version != 124 {
|
||||
t.Fatalf("migration status = %+v, want clean version 124", status)
|
||||
if status.Dirty || status.Empty || status.Version != 125 {
|
||||
t.Fatalf("migration status = %+v, want clean version 125", status)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1106
internal/store/postgres/telegram_login.go
Normal file
1106
internal/store/postgres/telegram_login.go
Normal file
File diff suppressed because it is too large
Load diff
434
internal/store/postgres/telegram_login_integration_test.go
Normal file
434
internal/store/postgres/telegram_login_integration_test.go
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func telegramLoginPGHash(value string) []byte {
|
||||
sum := sha256.Sum256([]byte(value))
|
||||
return sum[:]
|
||||
}
|
||||
|
||||
func TestTelegramLoginStorePostgresAtomicStateAndCodeConsumption(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
suffix := now.UnixNano() % 1_000_000_000
|
||||
|
||||
users := NewUserStore(pool)
|
||||
bots := NewBotStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{
|
||||
AccessHash: suffix + 101,
|
||||
Phone: fmt.Sprintf("1777%09d", suffix),
|
||||
FirstName: "OIDC Owner",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create oidc owner: %v", err)
|
||||
}
|
||||
bot, _, err := bots.CreateBotAccount(ctx, domain.User{
|
||||
AccessHash: suffix + 102,
|
||||
FirstName: "OIDC Test Bot",
|
||||
Username: fmt.Sprintf("oidc_%09d_bot", suffix),
|
||||
}, domain.BotProfile{OwnerUserID: owner.ID, TokenSecret: "bot-secret"})
|
||||
if err != nil {
|
||||
t.Fatalf("create oidc bot: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id IN ($1,$2)", owner.ID, bot.ID)
|
||||
})
|
||||
|
||||
store := NewTelegramLoginStore(pool)
|
||||
client, err := store.UpsertTelegramLoginClient(ctx, domain.TelegramLoginClient{
|
||||
BotUserID: bot.ID,
|
||||
ClientID: fmt.Sprintf("%d", bot.ID),
|
||||
SecretHash: telegramLoginPGHash("client-secret"),
|
||||
SecretVersion: 1,
|
||||
SigningAlgorithm: domain.TelegramLoginSigningRS256,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("upsert oidc client: %v", err)
|
||||
}
|
||||
redirectURI := fmt.Sprintf("https://rp-%d.example/callback", suffix)
|
||||
origin := fmt.Sprintf("https://rp-%d.example", suffix)
|
||||
if _, err := store.AddTelegramLoginAllowedURL(ctx, domain.TelegramLoginAllowedURL{
|
||||
BotUserID: client.BotUserID, Kind: domain.TelegramLoginAllowedRedirectURI,
|
||||
NormalizedURL: redirectURI, CreatedAt: now,
|
||||
}); err != nil {
|
||||
t.Fatalf("add redirect: %v", err)
|
||||
}
|
||||
if _, err := store.AddTelegramLoginAllowedURL(ctx, domain.TelegramLoginAllowedURL{
|
||||
BotUserID: client.BotUserID, Kind: domain.TelegramLoginAllowedWebOrigin,
|
||||
NormalizedURL: origin, CreatedAt: now,
|
||||
}); err != nil {
|
||||
t.Fatalf("add web origin: %v", err)
|
||||
}
|
||||
|
||||
newRequest := func(label string) domain.TelegramLoginRequest {
|
||||
request, err := store.CreateTelegramLoginRequest(ctx, domain.TelegramLoginRequest{
|
||||
RequestTokenHash: telegramLoginPGHash("request-" + label),
|
||||
BrowserTokenHash: telegramLoginPGHash("browser-" + label),
|
||||
BotUserID: bot.ID,
|
||||
ClientID: client.ClientID,
|
||||
SigningAlgorithm: client.SigningAlgorithm,
|
||||
Source: domain.TelegramLoginRequestWeb,
|
||||
ResponseType: "code",
|
||||
RedirectURI: redirectURI,
|
||||
Origin: origin,
|
||||
Domain: fmt.Sprintf("rp-%d.example", suffix),
|
||||
Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile, domain.TelegramLoginScopeBotAccess},
|
||||
State: "state",
|
||||
Nonce: "nonce",
|
||||
CodeChallenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
CodeChallengeMethod: "S256",
|
||||
Browser: "Firefox",
|
||||
Platform: "Windows",
|
||||
IP: "192.0.2.10",
|
||||
Region: "Test Region",
|
||||
MatchCodes: []string{"🟢", "🔵", "🟠"},
|
||||
MatchCode: "🔵",
|
||||
MatchCodesFirst: true,
|
||||
Status: domain.TelegramLoginRequestPending,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create request %s: %v", label, err)
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
request := newRequest(fmt.Sprintf("race-%d", suffix))
|
||||
start := make(chan struct{})
|
||||
errs := make(chan error, 2)
|
||||
go func() {
|
||||
<-start
|
||||
_, _, err := store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{
|
||||
RequestID: request.ID,
|
||||
Identity: domain.TelegramLoginIdentitySnapshot{UserID: owner.ID, Name: owner.FirstName, GivenName: owner.FirstName},
|
||||
WriteAllowed: true,
|
||||
MatchCode: request.MatchCode, ApprovedAt: now.Add(time.Second),
|
||||
}, suffix+10_000)
|
||||
errs <- err
|
||||
}()
|
||||
go func() {
|
||||
<-start
|
||||
_, err := store.DeclineTelegramLoginRequest(ctx, request.ID, owner.ID, now.Add(time.Second))
|
||||
errs <- err
|
||||
}()
|
||||
close(start)
|
||||
var success, conflict int
|
||||
for range 2 {
|
||||
err := <-errs
|
||||
switch {
|
||||
case err == nil:
|
||||
success++
|
||||
case errors.Is(err, domain.ErrTelegramLoginRequestConflict):
|
||||
conflict++
|
||||
default:
|
||||
t.Fatalf("accept/decline race error: %v", err)
|
||||
}
|
||||
}
|
||||
if success != 1 || conflict != 1 {
|
||||
t.Fatalf("accept/decline success=%d conflict=%d, want 1/1", success, conflict)
|
||||
}
|
||||
|
||||
codeRequest := newRequest(fmt.Sprintf("code-%d", suffix))
|
||||
_, web, err := store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{
|
||||
RequestID: codeRequest.ID,
|
||||
Identity: domain.TelegramLoginIdentitySnapshot{UserID: owner.ID, Name: owner.FirstName, GivenName: owner.FirstName},
|
||||
WriteAllowed: true,
|
||||
MatchCode: codeRequest.MatchCode, ApprovedAt: now.Add(2 * time.Second),
|
||||
}, suffix+20_000)
|
||||
if err != nil {
|
||||
t.Fatalf("approve code request: %v", err)
|
||||
}
|
||||
canSend, err := bots.CanBotSendMessage(ctx, bot.ID, owner.ID)
|
||||
if err != nil || !canSend {
|
||||
t.Fatalf("bot access after atomic approval = %v,%v", canSend, err)
|
||||
}
|
||||
code := domain.TelegramLoginAuthorizationCode{
|
||||
RequestID: codeRequest.ID,
|
||||
CodeHash: telegramLoginPGHash(fmt.Sprintf("code-%d", suffix)),
|
||||
SealedCode: append(make([]byte, 32), 1),
|
||||
SealNonce: make([]byte, 12),
|
||||
SealKeyID: "integration-key",
|
||||
IssuedAt: now.Add(3 * time.Second),
|
||||
ExpiresAt: now.Add(time.Minute),
|
||||
}
|
||||
if _, err := store.PutTelegramLoginAuthorizationCode(ctx, code); err != nil {
|
||||
t.Fatalf("put code: %v", err)
|
||||
}
|
||||
exchange := domain.TelegramLoginCodeExchange{
|
||||
CodeHash: code.CodeHash, ClientID: client.ClientID, ClientSecretVersion: client.SecretVersion,
|
||||
RedirectURI: codeRequest.RedirectURI, CodeChallenge: codeRequest.CodeChallenge, Now: now.Add(4 * time.Second),
|
||||
}
|
||||
start = make(chan struct{})
|
||||
errs = make(chan error, 8)
|
||||
var wg sync.WaitGroup
|
||||
for range 8 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
_, _, _, err := store.ConsumeTelegramLoginAuthorizationCode(ctx, exchange)
|
||||
errs <- err
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
success, conflict = 0, 0
|
||||
for err := range errs {
|
||||
switch {
|
||||
case err == nil:
|
||||
success++
|
||||
case errors.Is(err, domain.ErrTelegramLoginCodeConsumed):
|
||||
conflict++
|
||||
default:
|
||||
t.Fatalf("code consume race error: %v", err)
|
||||
}
|
||||
}
|
||||
if success != 1 || conflict != 7 {
|
||||
t.Fatalf("code consume success=%d consumed=%d, want 1/7", success, conflict)
|
||||
}
|
||||
|
||||
miniRequest, err := store.CreateTelegramLoginRequest(ctx, domain.TelegramLoginRequest{
|
||||
RequestTokenHash: telegramLoginPGHash(fmt.Sprintf("mini-request-%d", suffix)),
|
||||
BrowserTokenHash: telegramLoginPGHash(fmt.Sprintf("mini-browser-%d", suffix)),
|
||||
BotUserID: bot.ID, ClientID: client.ClientID, SigningAlgorithm: client.SigningAlgorithm,
|
||||
Source: domain.TelegramLoginRequestMiniApp, ResponseType: "post_message",
|
||||
RedirectURI: origin + "/", Origin: origin, InAppOrigin: origin, Domain: fmt.Sprintf("rp-%d.example", suffix),
|
||||
Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile},
|
||||
Browser: "Telegram Mini App", Platform: "Telegram Mini App", IP: "192.0.2.11", Region: "Test Region",
|
||||
MatchCodes: []string{"🟢", "🔵", "🟠"}, MatchCode: "🔵", MatchCodesFirst: true,
|
||||
Status: domain.TelegramLoginRequestPending, CreatedAt: now, ExpiresAt: now.Add(5 * time.Minute),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create mini-app request: %v", err)
|
||||
}
|
||||
if _, _, err := store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{
|
||||
RequestID: miniRequest.ID,
|
||||
Identity: domain.TelegramLoginIdentitySnapshot{UserID: owner.ID, Name: owner.FirstName, GivenName: owner.FirstName},
|
||||
MatchCode: miniRequest.MatchCode, ApprovedAt: now.Add(5 * time.Second),
|
||||
}, suffix+25_000); err != nil {
|
||||
t.Fatalf("approve mini-app request: %v", err)
|
||||
}
|
||||
directToken := domain.TelegramLoginAuthorizationCode{
|
||||
RequestID: miniRequest.ID, CodeHash: telegramLoginPGHash(fmt.Sprintf("mini-token-%d", suffix)),
|
||||
SealedCode: append(make([]byte, 32), 1), SealNonce: make([]byte, 12), SealKeyID: "integration-key",
|
||||
IssuedAt: now.Add(6 * time.Second), ExpiresAt: now.Add(time.Minute),
|
||||
}
|
||||
if _, err := store.PutTelegramLoginAuthorizationCode(ctx, directToken); err != nil {
|
||||
t.Fatalf("put mini-app token: %v", err)
|
||||
}
|
||||
start = make(chan struct{})
|
||||
errs = make(chan error, 8)
|
||||
for range 8 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
_, _, _, err := store.ConsumeTelegramLoginDirectToken(ctx, directToken.CodeHash, origin, now.Add(7*time.Second))
|
||||
errs <- err
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
success, conflict = 0, 0
|
||||
for err := range errs {
|
||||
switch {
|
||||
case err == nil:
|
||||
success++
|
||||
case errors.Is(err, domain.ErrTelegramLoginCodeConsumed):
|
||||
conflict++
|
||||
default:
|
||||
t.Fatalf("mini-app token consume race error: %v", err)
|
||||
}
|
||||
}
|
||||
if success != 1 || conflict != 7 {
|
||||
t.Fatalf("mini-app token consume success=%d consumed=%d, want 1/7", success, conflict)
|
||||
}
|
||||
|
||||
if revoked, err := store.RevokeTelegramLoginWebAuthorization(ctx, owner.ID, web.Hash, now.Add(5*time.Second)); err != nil || !revoked {
|
||||
t.Fatalf("revoke web authorization = %v,%v", revoked, err)
|
||||
}
|
||||
if listed, err := store.ListTelegramLoginWebAuthorizations(ctx, owner.ID); err != nil {
|
||||
t.Fatalf("list web authorizations: %v", err)
|
||||
} else {
|
||||
for _, got := range listed {
|
||||
if got.Hash == web.Hash {
|
||||
t.Fatalf("revoked web authorization still listed: %#v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTelegramLoginConfigDeleteTakesClientLock(t, pool, client.BotUserID, func() (bool, error) {
|
||||
return store.DeleteTelegramLoginAllowedURL(ctx, client.BotUserID, domain.TelegramLoginAllowedRedirectURI, redirectURI)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTelegramLoginStorePostgresNativeCallbackAndRetention(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
suffix := now.UnixNano() % 1_000_000_000
|
||||
|
||||
users := NewUserStore(pool)
|
||||
bots := NewBotStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{
|
||||
AccessHash: suffix + 301, Phone: fmt.Sprintf("1666%09d", suffix), FirstName: "Native Owner",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bot, _, err := bots.CreateBotAccount(ctx, domain.User{
|
||||
AccessHash: suffix + 302, FirstName: "Native Login Bot", Username: fmt.Sprintf("native_%09d_bot", suffix),
|
||||
}, domain.BotProfile{OwnerUserID: owner.ID, TokenSecret: "native-bot-secret"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM users WHERE id IN ($1,$2)", owner.ID, bot.ID) })
|
||||
|
||||
store := NewTelegramLoginStore(pool)
|
||||
client, err := store.CreateTelegramLoginClient(ctx, domain.TelegramLoginClient{
|
||||
BotUserID: bot.ID, ClientID: fmt.Sprintf("%d", bot.ID), SecretHash: telegramLoginPGHash("native-secret"),
|
||||
SecretVersion: 1, SigningAlgorithm: domain.TelegramLoginSigningRS256, Enabled: true,
|
||||
CreatedAt: now, UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const callbackURI = "bedolaga://telegram-login"
|
||||
nativeApp, err := store.UpsertTelegramLoginNativeApp(ctx, domain.TelegramLoginNativeApp{
|
||||
BotUserID: bot.ID, Platform: domain.TelegramLoginNativeAndroid, ApplicationID: "dev.bedolaga.demo",
|
||||
VerificationID: strings.Repeat("A", 64), CallbackURI: callbackURI, VerifiedDisplayName: "Bedolaga Demo",
|
||||
Enabled: true, CreatedAt: now, UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
createRequest := func(label string) domain.TelegramLoginRequest {
|
||||
t.Helper()
|
||||
request, err := store.CreateTelegramLoginRequest(ctx, domain.TelegramLoginRequest{
|
||||
RequestTokenHash: telegramLoginPGHash("native-request-" + label), BrowserTokenHash: telegramLoginPGHash("native-browser-" + label),
|
||||
BotUserID: bot.ID, ClientID: client.ClientID, SigningAlgorithm: client.SigningAlgorithm,
|
||||
Source: domain.TelegramLoginRequestNative, ResponseType: "code", RedirectURI: callbackURI,
|
||||
Domain: "dev.bedolaga.demo", Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile},
|
||||
CodeChallenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", CodeChallengeMethod: "S256",
|
||||
Browser: "TelegramLogin/Android", Platform: "Android", IP: "192.0.2.20", Region: "Test Region",
|
||||
IsApp: true, VerifiedAppName: "Bedolaga Demo", MatchCodes: []string{}, Status: domain.TelegramLoginRequestPending,
|
||||
CreatedAt: now, ExpiresAt: now.Add(5 * time.Minute),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create native request: %v", err)
|
||||
}
|
||||
return request
|
||||
}
|
||||
approve := func(request domain.TelegramLoginRequest, hash int64) domain.TelegramLoginWebAuthorization {
|
||||
t.Helper()
|
||||
_, web, err := store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{
|
||||
RequestID: request.ID, Identity: domain.TelegramLoginIdentitySnapshot{UserID: owner.ID, Name: "Native Owner", GivenName: "Native"},
|
||||
ApprovedAt: now.Add(time.Second),
|
||||
}, hash)
|
||||
if err != nil {
|
||||
t.Fatalf("approve native request: %v", err)
|
||||
}
|
||||
return web
|
||||
}
|
||||
|
||||
revokedRequest := createRequest(fmt.Sprintf("revoked-%d", suffix))
|
||||
revokedWeb := approve(revokedRequest, suffix+30_000)
|
||||
code := domain.TelegramLoginAuthorizationCode{
|
||||
RequestID: revokedRequest.ID, CodeHash: telegramLoginPGHash(fmt.Sprintf("native-code-%d", suffix)),
|
||||
SealedCode: append(make([]byte, 32), 1), SealNonce: make([]byte, 12), SealKeyID: "integration-key",
|
||||
IssuedAt: now.Add(2 * time.Second), ExpiresAt: now.Add(time.Minute),
|
||||
}
|
||||
if _, err := store.PutTelegramLoginAuthorizationCode(ctx, code); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, err := store.ConsumeTelegramLoginAuthorizationCode(ctx, domain.TelegramLoginCodeExchange{
|
||||
CodeHash: code.CodeHash, ClientID: client.ClientID, ClientSecretVersion: client.SecretVersion,
|
||||
RedirectURI: callbackURI, CodeChallenge: revokedRequest.CodeChallenge, Now: now.Add(3 * time.Second),
|
||||
}); err != nil {
|
||||
t.Fatalf("consume native code: %v", err)
|
||||
}
|
||||
if ok, err := store.RevokeTelegramLoginWebAuthorization(ctx, owner.ID, revokedWeb.Hash, now.Add(4*time.Second)); err != nil || !ok {
|
||||
t.Fatalf("revoke native authorization = %v,%v", ok, err)
|
||||
}
|
||||
|
||||
activeRequest := createRequest(fmt.Sprintf("active-%d", suffix))
|
||||
activeWeb := approve(activeRequest, suffix+40_000)
|
||||
deleted, err := store.DeleteExpiredTelegramLoginArtifacts(ctx, now.Add(2*time.Hour), 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if deleted < 2 {
|
||||
t.Fatalf("retention deleted=%d, want at least code and revoked request", deleted)
|
||||
}
|
||||
if _, found, _ := store.GetTelegramLoginRequest(ctx, revokedRequest.ID); found {
|
||||
t.Fatal("revoked native request survived retention")
|
||||
}
|
||||
if _, found, _ := store.GetTelegramLoginRequest(ctx, activeRequest.ID); !found {
|
||||
t.Fatal("active native request was deleted")
|
||||
}
|
||||
listed, err := store.ListTelegramLoginWebAuthorizations(ctx, owner.ID)
|
||||
if err != nil || len(listed) != 1 || listed[0].Hash != activeWeb.Hash {
|
||||
t.Fatalf("active authorization list=%#v err=%v", listed, err)
|
||||
}
|
||||
assertTelegramLoginConfigDeleteTakesClientLock(t, pool, client.BotUserID, func() (bool, error) {
|
||||
return store.DeleteTelegramLoginNativeApp(ctx, client.BotUserID, nativeApp.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func assertTelegramLoginConfigDeleteTakesClientLock(t *testing.T, pool *pgxpool.Pool, botUserID int64, remove func() (bool, error)) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
var lockedID int64
|
||||
if err := tx.QueryRow(ctx, `SELECT bot_user_id FROM bot_login_clients WHERE bot_user_id = $1 FOR UPDATE`, botUserID).Scan(&lockedID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
deleted, err := remove()
|
||||
if err == nil && !deleted {
|
||||
err = errors.New("configuration row was not deleted")
|
||||
}
|
||||
result <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-result:
|
||||
t.Fatalf("configuration delete bypassed client serialization lock: %v", err)
|
||||
case <-time.After(150 * time.Millisecond):
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-result:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("configuration delete remained blocked after client lock committed")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue