Files
yuan-lab-llm--clawmanager/backend/internal/services/instance_external_access_service_test.go
wehub-resource-sync 6d978fe483
Build / Frontend Build (push) Has been cancelled
Build / Backend Build (push) Has been cancelled
Build / Docker Build (push) Has been cancelled
Build / Kubernetes Deploy Smoke Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:24 +08:00

305 lines
10 KiB
Go

package services
import (
"context"
"strings"
"testing"
"time"
"clawreef/internal/models"
)
func TestInstanceExternalAccessShareLinkValidation(t *testing.T) {
repo := newFakeExternalAccessRepo()
service := NewInstanceExternalAccessService(repo)
result, err := service.EnableShareLink(context.Background(), 42, 7, ExternalAccessExpirationRequest{Mode: ExternalAccessExpirationPermanent})
if err != nil {
t.Fatalf("EnableShareLink failed: %v", err)
}
if !strings.HasPrefix(result.ShareURL, "/s/") || strings.Contains(result.ShareURL, "token=") || strings.Contains(result.ShareURL, "/api/v1/") || strings.Contains(result.ShareURL, "/proxy") {
t.Fatalf("unexpected short share URL: %q", result.ShareURL)
}
code := strings.Trim(strings.TrimPrefix(result.ShareURL, "/s/"), "/")
if code == "" {
t.Fatalf("short code missing from URL %q", result.ShareURL)
}
if result.Access.ShortCodeHash == nil || *result.Access.ShortCodeHash == code {
t.Fatalf("expected stored short code to be hashed")
}
if result.Access.AuthMode != ExternalAccessModeShareLink {
t.Fatalf("auth mode = %q, want %q", result.Access.AuthMode, ExternalAccessModeShareLink)
}
access, err := service.ValidateShortLink(context.Background(), code, "")
if err != nil {
t.Fatalf("ValidateShortLink failed: %v", err)
}
if access.InstanceID != 42 {
t.Fatalf("validated instance ID = %d, want 42", access.InstanceID)
}
if repo.markUsedCount != 1 {
t.Fatalf("mark used count = %d, want 1", repo.markUsedCount)
}
if _, err := service.ValidateShortLink(context.Background(), "wrong-code", ""); err == nil {
t.Fatalf("expected wrong short code to fail")
}
}
func TestInstanceExternalAccessShareLinkPresetExpiration(t *testing.T) {
repo := newFakeExternalAccessRepo()
service := NewInstanceExternalAccessService(repo)
before := time.Now().UTC().Add(24 * time.Hour)
result, err := service.EnableShareLink(context.Background(), 42, 7, ExternalAccessExpirationRequest{
Mode: ExternalAccessExpirationPreset,
Preset: ExternalAccessPreset24Hours,
})
if err != nil {
t.Fatalf("EnableShareLink failed: %v", err)
}
if result.Access.ExpiresAt == nil {
t.Fatal("preset expiration must set expires_at")
}
after := time.Now().UTC().Add(24 * time.Hour)
if result.Access.ExpiresAt.Before(before) || result.Access.ExpiresAt.After(after.Add(time.Second)) {
t.Fatalf("preset expires_at = %v, want around 24h from now", result.Access.ExpiresAt)
}
}
func TestInstanceExternalAccessShareLinkCustomAndPermanentExpiration(t *testing.T) {
repo := newFakeExternalAccessRepo()
service := NewInstanceExternalAccessService(repo)
customAt := time.Now().UTC().Add(3 * time.Hour).Truncate(time.Second)
custom, err := service.EnableShareLink(context.Background(), 42, 7, ExternalAccessExpirationRequest{
Mode: ExternalAccessExpirationCustom,
ExpiresAt: &customAt,
})
if err != nil {
t.Fatalf("custom EnableShareLink failed: %v", err)
}
if custom.Access.ExpiresAt == nil || !custom.Access.ExpiresAt.Equal(customAt) {
t.Fatalf("custom expires_at = %v, want %v", custom.Access.ExpiresAt, customAt)
}
permanent, err := service.EnableShareLink(context.Background(), 42, 7, ExternalAccessExpirationRequest{Mode: ExternalAccessExpirationPermanent})
if err != nil {
t.Fatalf("permanent EnableShareLink failed: %v", err)
}
if permanent.Access.ExpiresAt != nil {
t.Fatalf("permanent expires_at = %v, want nil", permanent.Access.ExpiresAt)
}
}
func TestInstanceExternalAccessPasswordDisable(t *testing.T) {
repo := newFakeExternalAccessRepo()
service := NewInstanceExternalAccessService(repo)
result, err := service.CreatePassword(context.Background(), 100, 9, ExternalAccessExpirationRequest{Mode: ExternalAccessExpirationPermanent})
if err != nil {
t.Fatalf("CreatePassword failed: %v", err)
}
if !strings.HasPrefix(result.Password, "pwd_") {
t.Fatalf("unexpected password prefix: %q", result.Password)
}
if result.Access.PasswordHash == nil || *result.Access.PasswordHash == result.Password {
t.Fatalf("expected stored password to be hashed")
}
if result.Access.PasswordHint == nil || !strings.HasPrefix(result.Password, *result.Access.PasswordHint) {
t.Fatalf("stored password hint does not match generated password")
}
if result.Access.AuthMode != ExternalAccessModePassword {
t.Fatalf("auth mode = %q, want %q", result.Access.AuthMode, ExternalAccessModePassword)
}
if !strings.HasPrefix(result.ShareURL, "/s/") || strings.Contains(result.ShareURL, "token=") || strings.Contains(result.ShareURL, "/api/v1/") {
t.Fatalf("unexpected password short URL: %q", result.ShareURL)
}
code := strings.Trim(strings.TrimPrefix(result.ShareURL, "/s/"), "/")
if _, err := service.ValidateShortLink(context.Background(), code, result.Password); err != nil {
t.Fatalf("ValidateShortLink with password failed: %v", err)
}
if err := service.Disable(context.Background(), 100); err != nil {
t.Fatalf("Disable failed: %v", err)
}
if _, err := service.ValidateShortLink(context.Background(), code, result.Password); err == nil {
t.Fatalf("expected disabled password access to fail")
}
}
func TestInstanceExternalAccessRestoresShareURLAfterRefresh(t *testing.T) {
repo := newFakeExternalAccessRepo()
service := NewInstanceExternalAccessService(repo)
result, err := service.CreatePassword(context.Background(), 100, 9, ExternalAccessExpirationRequest{Mode: ExternalAccessExpirationPermanent})
if err != nil {
t.Fatalf("CreatePassword failed: %v", err)
}
reloaded, err := service.Get(context.Background(), 100)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if got := ExternalAccessShareURL(reloaded); got != result.ShareURL {
t.Fatalf("ExternalAccessShareURL after reload = %q, want %q", got, result.ShareURL)
}
}
func TestInstanceExternalAccessRestoresPasswordAfterRefresh(t *testing.T) {
repo := newFakeExternalAccessRepo()
service := NewInstanceExternalAccessService(repo)
result, err := service.CreatePassword(context.Background(), 100, 9, ExternalAccessExpirationRequest{Mode: ExternalAccessExpirationPermanent})
if err != nil {
t.Fatalf("CreatePassword failed: %v", err)
}
reloaded, err := service.Get(context.Background(), 100)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if got := ExternalAccessPassword(reloaded); got != result.Password {
t.Fatalf("ExternalAccessPassword after reload = %q, want generated password", got)
}
}
func TestInstanceExternalAccessExpiration(t *testing.T) {
repo := newFakeExternalAccessRepo()
service := NewInstanceExternalAccessService(repo)
expiredAt := time.Now().UTC().Add(-time.Minute)
result, err := service.EnableShareLink(context.Background(), 77, 3, ExternalAccessExpirationRequest{
Mode: ExternalAccessExpirationCustom,
ExpiresAt: &expiredAt,
})
if err != nil {
t.Fatalf("EnableShareLink failed: %v", err)
}
code := strings.Trim(strings.TrimPrefix(result.ShareURL, "/s/"), "/")
if _, err := service.ValidateShortLink(context.Background(), code, ""); err == nil {
t.Fatalf("expected expired public access to fail")
}
}
type fakeExternalAccessRepo struct {
byInstance map[int]*models.InstanceExternalAccess
byCodeHash map[string]*models.InstanceExternalAccess
nextID int64
markUsedCount int
}
func newFakeExternalAccessRepo() *fakeExternalAccessRepo {
return &fakeExternalAccessRepo{
byInstance: make(map[int]*models.InstanceExternalAccess),
byCodeHash: make(map[string]*models.InstanceExternalAccess),
nextID: 1,
}
}
func (r *fakeExternalAccessRepo) GetByInstanceID(ctx context.Context, instanceID int) (*models.InstanceExternalAccess, error) {
_ = ctx
return cloneExternalAccess(r.byInstance[instanceID]), nil
}
func (r *fakeExternalAccessRepo) GetByShortCodeHash(ctx context.Context, codeHash string) (*models.InstanceExternalAccess, error) {
_ = ctx
return cloneExternalAccess(r.byCodeHash[codeHash]), nil
}
func (r *fakeExternalAccessRepo) Upsert(ctx context.Context, access *models.InstanceExternalAccess) error {
_ = ctx
saved := cloneExternalAccess(access)
if saved.ID == 0 {
if existing := r.byInstance[saved.InstanceID]; existing != nil {
saved.ID = existing.ID
} else {
saved.ID = r.nextID
r.nextID++
}
}
if existing := r.byInstance[saved.InstanceID]; existing != nil && existing.ShortCodeHash != nil {
delete(r.byCodeHash, *existing.ShortCodeHash)
}
r.byInstance[saved.InstanceID] = cloneExternalAccess(saved)
if saved.ShortCodeHash != nil {
r.byCodeHash[*saved.ShortCodeHash] = cloneExternalAccess(saved)
}
return nil
}
func (r *fakeExternalAccessRepo) Disable(ctx context.Context, instanceID int) error {
_ = ctx
if existing := r.byInstance[instanceID]; existing != nil {
existing.Enabled = false
if existing.ShortCodeHash != nil {
if byCode := r.byCodeHash[*existing.ShortCodeHash]; byCode != nil {
byCode.Enabled = false
}
}
}
return nil
}
func (r *fakeExternalAccessRepo) MarkUsed(ctx context.Context, id int64) error {
_ = ctx
r.markUsedCount++
now := time.Now().UTC()
for _, access := range r.byInstance {
if access.ID == id {
access.LastUsedAt = &now
}
}
for _, access := range r.byCodeHash {
if access.ID == id {
access.LastUsedAt = &now
}
}
return nil
}
func cloneExternalAccess(access *models.InstanceExternalAccess) *models.InstanceExternalAccess {
if access == nil {
return nil
}
clone := *access
if access.PublicSlug != nil {
value := *access.PublicSlug
clone.PublicSlug = &value
}
if access.PublicTokenHash != nil {
value := *access.PublicTokenHash
clone.PublicTokenHash = &value
}
if access.ShortCodeHash != nil {
value := *access.ShortCodeHash
clone.ShortCodeHash = &value
}
if access.PasswordHash != nil {
value := *access.PasswordHash
clone.PasswordHash = &value
}
if access.PasswordValue != nil {
value := *access.PasswordValue
clone.PasswordValue = &value
}
if access.PasswordHint != nil {
value := *access.PasswordHint
clone.PasswordHint = &value
}
if access.ExpiresAt != nil {
value := *access.ExpiresAt
clone.ExpiresAt = &value
}
if access.CreatedBy != nil {
value := *access.CreatedBy
clone.CreatedBy = &value
}
if access.LastUsedAt != nil {
value := *access.LastUsedAt
clone.LastUsedAt = &value
}
return &clone
}