2026-07-16 01:54:42 -03:00
package k8s
import (
"context"
"errors"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
k8stesting "k8s.io/client-go/testing"
)
func TestGetLonghornBackupBySnapshotCoversLookupPaths ( t * testing . T ) {
matchingOlder := longhornBackupObject ( "backup-old" , "snap-a" , "Completed" , "" , 100 , "2026-04-20T10:00:00Z" )
matchingNewer := longhornBackupObject ( "backup-new" , "snap-a" , "Error" , "storage cap exceeded" , 0 , "2026-04-20T11:00:00Z" )
other := longhornBackupObject ( "backup-other" , "snap-b" , "Completed" , "" , 100 , "2026-04-20T12:00:00Z" )
client := & Client { Dynamic : newLonghornBackupDynamic ( matchingOlder , matchingNewer , other ) }
backup , found , err := client . GetLonghornBackupBySnapshot ( context . Background ( ) , "snap-a" )
if err != nil || ! found {
t . Fatalf ( "expected matching backup, found=%v err=%v" , found , err )
}
if backup . Name != "backup-new" || backup . State != "Error" || backup . Error != "storage cap exceeded" || backup . Progress != 0 {
t . Fatalf ( "expected newest matching backup with status fields, got %#v" , backup )
}
if _ , found , err := client . GetLonghornBackupBySnapshot ( context . Background ( ) , "missing" ) ; err != nil || found {
t . Fatalf ( "expected missing snapshot to be cleanly absent, found=%v err=%v" , found , err )
}
}
func TestGetLonghornBackupBySnapshotValidatesClientAndInput ( t * testing . T ) {
client := & Client { Dynamic : newLonghornBackupDynamic ( ) }
if _ , _ , err := client . GetLonghornBackupBySnapshot ( context . Background ( ) , " " ) ; err == nil || ! strings . Contains ( err . Error ( ) , "snapshot name is required" ) {
t . Fatalf ( "expected snapshot validation error, got %v" , err )
}
client . Dynamic = nil
if _ , _ , err := client . GetLonghornBackupBySnapshot ( context . Background ( ) , "snap-a" ) ; err == nil || ! strings . Contains ( err . Error ( ) , "dynamic Kubernetes client is unavailable" ) {
t . Fatalf ( "expected missing dynamic client error, got %v" , err )
}
}
func TestGetLonghornBackupBySnapshotWrapsListErrorAndFallbackTimestamp ( t * testing . T ) {
client := & Client { Dynamic : newLonghornBackupDynamic ( longhornBackupObject ( "backup-a" , "snap-a" , "Completed" , "" , 100 , "" ) ) }
backup , found , err := client . GetLonghornBackupBySnapshot ( context . Background ( ) , "snap-a" )
if err != nil || ! found {
t . Fatalf ( "expected fallback timestamp backup, found=%v err=%v" , found , err )
}
if backup . CreatedAt == "" || backup . SnapshotName != "snap-a" {
t . Fatalf ( "expected fallback timestamp and snapshot name, got %#v" , backup )
}
failingDynamic := newLonghornBackupDynamic ( )
failingDynamic . PrependReactor ( "list" , "backups" , func ( k8stesting . Action ) ( bool , runtime . Object , error ) {
return true , nil , errors . New ( "list exploded" )
} )
client . Dynamic = failingDynamic
if _ , _ , err := client . GetLonghornBackupBySnapshot ( context . Background ( ) , "snap-a" ) ; err == nil || ! strings . Contains ( err . Error ( ) , "list exploded" ) {
t . Fatalf ( "expected wrapped list error, got %v" , err )
}
}
2026-07-16 02:08:46 -03:00
func TestListLonghornBackupsFiltersSoteriaLabelsAndSorts ( t * testing . T ) {
older := longhornBackupObject ( "backup-older" , "snap-a" , "Completed" , "" , 100 , "2026-04-20T10:00:00Z" )
newer := longhornBackupObject ( "backup-newer" , "snap-b" , "Error" , "storage cap exceeded" , 0 , "2026-04-20T11:00:00Z" )
setLonghornBackupLabels ( newer , "ops" , "cache" )
unlabeled := longhornBackupObject ( "backup-unlabeled" , "snap-c" , "Completed" , "" , 100 , "2026-04-20T12:00:00Z" )
_ = unstructured . SetNestedStringMap ( unlabeled . Object , map [ string ] string { } , "spec" , "labels" )
client := & Client { Dynamic : newLonghornBackupDynamic ( older , newer , unlabeled ) }
backups , err := client . ListLonghornBackups ( context . Background ( ) )
if err != nil {
t . Fatalf ( "list Longhorn backups: %v" , err )
}
if len ( backups ) != 2 {
t . Fatalf ( "expected two Soteria-labeled backups, got %#v" , backups )
}
if backups [ 0 ] . Name != "backup-newer" || backups [ 0 ] . Namespace != "ops" || backups [ 0 ] . PVC != "cache" {
t . Fatalf ( "expected newest ops/cache backup first, got %#v" , backups [ 0 ] )
}
if backups [ 1 ] . Name != "backup-older" || backups [ 1 ] . Namespace != "apps" || backups [ 1 ] . PVC != "data" {
t . Fatalf ( "expected older apps/data backup second, got %#v" , backups [ 1 ] )
}
}
func TestListLonghornBackupsValidatesClientAndWrapsListError ( t * testing . T ) {
client := & Client { }
if _ , err := client . ListLonghornBackups ( context . Background ( ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , "dynamic Kubernetes client is unavailable" ) {
t . Fatalf ( "expected missing dynamic client error, got %v" , err )
}
failingDynamic := newLonghornBackupDynamic ( )
failingDynamic . PrependReactor ( "list" , "backups" , func ( k8stesting . Action ) ( bool , runtime . Object , error ) {
return true , nil , errors . New ( "list exploded" )
} )
client . Dynamic = failingDynamic
if _ , err := client . ListLonghornBackups ( context . Background ( ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , "list exploded" ) {
t . Fatalf ( "expected wrapped list error, got %v" , err )
}
}
2026-07-16 01:54:42 -03:00
func newLonghornBackupDynamic ( objects ... runtime . Object ) * dynamicfake . FakeDynamicClient {
return dynamicfake . NewSimpleDynamicClientWithCustomListKinds (
runtime . NewScheme ( ) ,
map [ schema . GroupVersionResource ] string { longhornBackupGVR : "BackupList" } ,
objects ... ,
)
}
2026-07-16 02:08:46 -03:00
func setLonghornBackupLabels ( item * unstructured . Unstructured , namespace , pvc string ) {
_ = unstructured . SetNestedStringMap ( item . Object , map [ string ] string {
"soteria.bstein.dev/namespace" : namespace ,
"soteria.bstein.dev/pvc" : pvc ,
} , "spec" , "labels" )
}
2026-07-16 01:54:42 -03:00
func longhornBackupObject ( name , snapshotName , state , errorMessage string , progress int64 , createdAt string ) * unstructured . Unstructured {
item := & unstructured . Unstructured { Object : map [ string ] any {
"apiVersion" : "longhorn.io/v1beta2" ,
"kind" : "Backup" ,
"metadata" : map [ string ] any {
"name" : name ,
"namespace" : longhornSystemNamespace ,
} ,
"spec" : map [ string ] any {
"snapshotName" : snapshotName ,
2026-07-16 02:08:46 -03:00
"labels" : map [ string ] any {
"soteria.bstein.dev/namespace" : "apps" ,
"soteria.bstein.dev/pvc" : "data" ,
} ,
2026-07-16 01:54:42 -03:00
} ,
"status" : map [ string ] any {
"state" : state ,
"error" : errorMessage ,
"progress" : progress ,
} ,
} }
if createdAt != "" {
_ = unstructured . SetNestedField ( item . Object , createdAt , "status" , "backupCreatedAt" )
}
item . SetCreationTimestamp ( metav1 . Now ( ) )
return item
}