mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			1016 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1016 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package db
 | 
						||
 | 
						||
import (
 | 
						||
	"database/sql"
 | 
						||
	"net/netip"
 | 
						||
	"os"
 | 
						||
	"os/exec"
 | 
						||
	"path/filepath"
 | 
						||
	"slices"
 | 
						||
	"strings"
 | 
						||
	"testing"
 | 
						||
	"time"
 | 
						||
 | 
						||
	"github.com/google/go-cmp/cmp"
 | 
						||
	"github.com/google/go-cmp/cmp/cmpopts"
 | 
						||
	"github.com/juanfont/headscale/hscontrol/types"
 | 
						||
	"github.com/juanfont/headscale/hscontrol/util"
 | 
						||
	"github.com/stretchr/testify/assert"
 | 
						||
	"github.com/stretchr/testify/require"
 | 
						||
	"gorm.io/gorm"
 | 
						||
	"zgo.at/zcache/v2"
 | 
						||
)
 | 
						||
 | 
						||
// TestSQLiteMigrationAndDataValidation tests specific SQLite migration scenarios
 | 
						||
// and validates data integrity after migration. All migrations that require data validation
 | 
						||
// should be added here.
 | 
						||
func TestSQLiteMigrationAndDataValidation(t *testing.T) {
 | 
						||
	ipp := func(p string) netip.Prefix {
 | 
						||
		return netip.MustParsePrefix(p)
 | 
						||
	}
 | 
						||
	r := func(id uint64, p string, a, e, i bool) types.Route {
 | 
						||
		return types.Route{
 | 
						||
			NodeID:     id,
 | 
						||
			Prefix:     ipp(p),
 | 
						||
			Advertised: a,
 | 
						||
			Enabled:    e,
 | 
						||
			IsPrimary:  i,
 | 
						||
		}
 | 
						||
	}
 | 
						||
	tests := []struct {
 | 
						||
		dbPath   string
 | 
						||
		wantFunc func(*testing.T, *HSDatabase)
 | 
						||
	}{
 | 
						||
		{
 | 
						||
			dbPath: "testdata/sqlite/0-22-3-to-0-23-0-routes-are-dropped-2063_dump.sql",
 | 
						||
			wantFunc: func(t *testing.T, hsdb *HSDatabase) {
 | 
						||
				t.Helper()
 | 
						||
				// Comprehensive data preservation validation for 0.22.3->0.23.0 migration
 | 
						||
				// Expected data from dump: 4 users, 17 pre_auth_keys, 14 machines/nodes, 12 routes
 | 
						||
 | 
						||
				// Verify users data preservation - should have 4 users
 | 
						||
				users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
 | 
						||
					return ListUsers(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, users, 4, "should preserve all 4 users from original schema")
 | 
						||
 | 
						||
				// Verify pre_auth_keys data preservation - should have 17 keys
 | 
						||
				preAuthKeys, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
 | 
						||
					var keys []types.PreAuthKey
 | 
						||
					err := rx.Find(&keys).Error
 | 
						||
					return keys, err
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, preAuthKeys, 17, "should preserve all 17 pre_auth_keys from original schema")
 | 
						||
 | 
						||
				// Verify all nodes data preservation - should have 14 nodes
 | 
						||
				allNodes, err := Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
 | 
						||
					return ListNodes(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, allNodes, 14, "should preserve all 14 machines/nodes from original schema")
 | 
						||
 | 
						||
				// Verify specific nodes and their route migration with detailed validation
 | 
						||
				nodes, err := Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
 | 
						||
					n1, err := GetNodeByID(rx, 1)
 | 
						||
					n26, err := GetNodeByID(rx, 26)
 | 
						||
					n31, err := GetNodeByID(rx, 31)
 | 
						||
					n32, err := GetNodeByID(rx, 32)
 | 
						||
					if err != nil {
 | 
						||
						return nil, err
 | 
						||
					}
 | 
						||
 | 
						||
					return types.Nodes{n1, n26, n31, n32}, nil
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, nodes, 4, "should have retrieved 4 specific nodes")
 | 
						||
 | 
						||
				// Validate specific node data from dump file
 | 
						||
				nodesByID := make(map[uint64]*types.Node)
 | 
						||
				for i := range nodes {
 | 
						||
					nodesByID[nodes[i].ID.Uint64()] = nodes[i]
 | 
						||
				}
 | 
						||
 | 
						||
				node1 := nodesByID[1]
 | 
						||
				node26 := nodesByID[26]
 | 
						||
				node31 := nodesByID[31]
 | 
						||
				node32 := nodesByID[32]
 | 
						||
 | 
						||
				require.NotNil(t, node1, "node 1 should exist")
 | 
						||
				require.NotNil(t, node26, "node 26 should exist")
 | 
						||
				require.NotNil(t, node31, "node 31 should exist")
 | 
						||
				require.NotNil(t, node32, "node 32 should exist")
 | 
						||
 | 
						||
				// Validate node data using cmp.Diff
 | 
						||
				expectedNodes := map[uint64]struct {
 | 
						||
					Hostname  string
 | 
						||
					GivenName string
 | 
						||
					IPv4      string
 | 
						||
				}{
 | 
						||
					1:  {Hostname: "test_hostname", GivenName: "test_given_name", IPv4: "100.64.0.1"},
 | 
						||
					26: {Hostname: "test_hostname", GivenName: "test_given_name", IPv4: "100.64.0.19"},
 | 
						||
					31: {Hostname: "test_hostname", GivenName: "test_given_name", IPv4: "100.64.0.7"},
 | 
						||
					32: {Hostname: "test_hostname", GivenName: "test_given_name", IPv4: "100.64.0.11"},
 | 
						||
				}
 | 
						||
 | 
						||
				for nodeID, expected := range expectedNodes {
 | 
						||
					node := nodesByID[nodeID]
 | 
						||
					require.NotNil(t, node, "node %d should exist", nodeID)
 | 
						||
 | 
						||
					actual := struct {
 | 
						||
						Hostname  string
 | 
						||
						GivenName string
 | 
						||
						IPv4      string
 | 
						||
					}{
 | 
						||
						Hostname:  node.Hostname,
 | 
						||
						GivenName: node.GivenName,
 | 
						||
						IPv4:      node.IPv4.String(),
 | 
						||
					}
 | 
						||
 | 
						||
					if diff := cmp.Diff(expected, actual); diff != "" {
 | 
						||
						t.Errorf("TestSQLiteMigrationAndDataValidation() node %d mismatch (-want +got):\n%s", nodeID, diff)
 | 
						||
					}
 | 
						||
				}
 | 
						||
 | 
						||
				// Validate that routes were properly migrated from routes table to approved_routes
 | 
						||
				// Based on the dump file routes data:
 | 
						||
				// Node 1 (machine_id 1): routes 1,2,3 (0.0.0.0/0 enabled, ::/0 enabled, 10.9.110.0/24 enabled+primary)
 | 
						||
				// Node 26 (machine_id 26): route 6 (172.100.100.0/24 enabled+primary), route 7 (172.100.100.0/24 disabled)
 | 
						||
				// Node 31 (machine_id 31): routes 8,10 (0.0.0.0/0 enabled, ::/0 enabled), routes 9,11 (duplicates disabled)
 | 
						||
				// Node 32 (machine_id 32): route 12 (192.168.0.24/32 enabled+primary)
 | 
						||
				want := [][]netip.Prefix{
 | 
						||
					{ipp("0.0.0.0/0"), ipp("10.9.110.0/24"), ipp("::/0")}, // node 1: 3 enabled routes
 | 
						||
					{ipp("172.100.100.0/24")},                             // node 26: 1 enabled route
 | 
						||
					{ipp("0.0.0.0/0"), ipp("::/0")},                       // node 31: 2 enabled routes
 | 
						||
					{ipp("192.168.0.24/32")},                              // node 32: 1 enabled route
 | 
						||
				}
 | 
						||
				var got [][]netip.Prefix
 | 
						||
				for _, node := range nodes {
 | 
						||
					got = append(got, node.ApprovedRoutes)
 | 
						||
				}
 | 
						||
 | 
						||
				if diff := cmp.Diff(want, got, util.PrefixComparer); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() route migration mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Verify routes table was dropped after migration
 | 
						||
				var routesTableExists bool
 | 
						||
				err = hsdb.DB.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='routes'").Row().Scan(&routesTableExists)
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.False(t, routesTableExists, "routes table should have been dropped after migration")
 | 
						||
			},
 | 
						||
		},
 | 
						||
		{
 | 
						||
			dbPath: "testdata/sqlite/0-22-3-to-0-23-0-routes-fail-foreign-key-2076_dump.sql",
 | 
						||
			wantFunc: func(t *testing.T, hsdb *HSDatabase) {
 | 
						||
				t.Helper()
 | 
						||
				// Comprehensive data preservation validation for foreign key constraint issue case
 | 
						||
				// Expected data from dump: 4 users, 2 pre_auth_keys, 8 nodes
 | 
						||
 | 
						||
				// Verify users data preservation
 | 
						||
				users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
 | 
						||
					return ListUsers(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, users, 4, "should preserve all 4 users from original schema")
 | 
						||
 | 
						||
				// Verify pre_auth_keys data preservation
 | 
						||
				preAuthKeys, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
 | 
						||
					var keys []types.PreAuthKey
 | 
						||
					err := rx.Find(&keys).Error
 | 
						||
					return keys, err
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, preAuthKeys, 2, "should preserve all 2 pre_auth_keys from original schema")
 | 
						||
 | 
						||
				// Verify all nodes data preservation
 | 
						||
				allNodes, err := Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
 | 
						||
					return ListNodes(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, allNodes, 8, "should preserve all 8 nodes from original schema")
 | 
						||
 | 
						||
				// Verify specific node route migration
 | 
						||
				node, err := Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
 | 
						||
					return GetNodeByID(rx, 13)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
 | 
						||
				assert.Len(t, node.ApprovedRoutes, 3)
 | 
						||
				_ = types.Routes{
 | 
						||
					// These routes exists, but have no nodes associated with them
 | 
						||
					// when the migration starts.
 | 
						||
					// r(1, "0.0.0.0/0", true, false),
 | 
						||
					// r(1, "::/0", true, false),
 | 
						||
					// r(3, "0.0.0.0/0", true, false),
 | 
						||
					// r(3, "::/0", true, false),
 | 
						||
					// r(5, "0.0.0.0/0", true, false),
 | 
						||
					// r(5, "::/0", true, false),
 | 
						||
					// r(6, "0.0.0.0/0", true, false),
 | 
						||
					// r(6, "::/0", true, false),
 | 
						||
					// r(6, "10.0.0.0/8", true, false, false),
 | 
						||
					// r(7, "0.0.0.0/0", true, false),
 | 
						||
					// r(7, "::/0", true, false),
 | 
						||
					// r(7, "10.0.0.0/8", true, false, false),
 | 
						||
					// r(9, "0.0.0.0/0", true, false),
 | 
						||
					// r(9, "::/0", true, false),
 | 
						||
					// r(9, "10.0.0.0/8", true, false),
 | 
						||
					// r(11, "0.0.0.0/0", true, false),
 | 
						||
					// r(11, "::/0", true, false),
 | 
						||
					// r(11, "10.0.0.0/8", true, true),
 | 
						||
					// r(12, "0.0.0.0/0", true, false),
 | 
						||
					// r(12, "::/0", true, false),
 | 
						||
					// r(12, "10.0.0.0/8", true, false, false),
 | 
						||
					//
 | 
						||
					// These nodes exists, so routes should be kept.
 | 
						||
					r(13, "10.0.0.0/8", true, false, false),
 | 
						||
					r(13, "0.0.0.0/0", true, true, false),
 | 
						||
					r(13, "::/0", true, true, false),
 | 
						||
					r(13, "10.18.80.2/32", true, true, true),
 | 
						||
				}
 | 
						||
				want := []netip.Prefix{ipp("0.0.0.0/0"), ipp("10.18.80.2/32"), ipp("::/0")}
 | 
						||
				if diff := cmp.Diff(want, node.ApprovedRoutes, util.PrefixComparer); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() route migration mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Verify routes table was dropped after migration
 | 
						||
				var routesTableExists bool
 | 
						||
				err = hsdb.DB.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='routes'").Row().Scan(&routesTableExists)
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.False(t, routesTableExists, "routes table should have been dropped after migration")
 | 
						||
			},
 | 
						||
		},
 | 
						||
		// at 14:15:06 ❯ go run ./cmd/headscale preauthkeys list
 | 
						||
		// ID | Key      | Reusable | Ephemeral | Used  | Expiration | Created    | Tags
 | 
						||
		// 1  | 09b28f.. | false    | false     | false | 2024-09-27 | 2024-09-27 | tag:derp
 | 
						||
		// 2  | 3112b9.. | false    | false     | false | 2024-09-27 | 2024-09-27 | tag:derp
 | 
						||
		// 3  | 7c23b9.. | false    | false     | false | 2024-09-27 | 2024-09-27 | tag:derp,tag:merp
 | 
						||
		// 4  | f20155.. | false    | false     | false | 2024-09-27 | 2024-09-27 | tag:test
 | 
						||
		// 5  | b212b9.. | false    | false     | false | 2024-09-27 | 2024-09-27 | tag:test,tag:woop,tag:dedu
 | 
						||
		{
 | 
						||
			dbPath: "testdata/sqlite/0-23-0-to-0-24-0-preauthkey-tags-table_dump.sql",
 | 
						||
			wantFunc: func(t *testing.T, hsdb *HSDatabase) {
 | 
						||
				t.Helper()
 | 
						||
				// Comprehensive data preservation validation for pre-auth key tags migration
 | 
						||
				// Expected data from dump: 2 users (kratest, testkra), 5 pre_auth_keys with specific tags
 | 
						||
 | 
						||
				// Verify users data preservation with specific user data
 | 
						||
				users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
 | 
						||
					return ListUsers(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, users, 2, "should preserve all 2 users from original schema")
 | 
						||
 | 
						||
				// Validate specific user data from dump file using cmp.Diff
 | 
						||
				expectedUsers := []types.User{
 | 
						||
					{Model: gorm.Model{ID: 1}, Name: "kratest"},
 | 
						||
					{Model: gorm.Model{ID: 2}, Name: "testkra"},
 | 
						||
				}
 | 
						||
 | 
						||
				if diff := cmp.Diff(expectedUsers, users,
 | 
						||
					cmpopts.IgnoreFields(types.User{}, "CreatedAt", "UpdatedAt", "DeletedAt", "DisplayName", "Email", "ProviderIdentifier", "Provider", "ProfilePicURL")); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() users mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Create maps for easier access in later validations
 | 
						||
				usersByName := make(map[string]*types.User)
 | 
						||
				for i := range users {
 | 
						||
					usersByName[users[i].Name] = &users[i]
 | 
						||
				}
 | 
						||
				kratest := usersByName["kratest"]
 | 
						||
				testkra := usersByName["testkra"]
 | 
						||
 | 
						||
				// Verify all pre_auth_keys data preservation
 | 
						||
				allKeys, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
 | 
						||
					var keys []types.PreAuthKey
 | 
						||
					err := rx.Find(&keys).Error
 | 
						||
					return keys, err
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, allKeys, 5, "should preserve all 5 pre_auth_keys from original schema")
 | 
						||
 | 
						||
				// Verify specific pre-auth keys and their tag migration with exact data validation
 | 
						||
				keys, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
 | 
						||
					kratest, err := ListPreAuthKeysByUser(rx, 1) // kratest
 | 
						||
					if err != nil {
 | 
						||
						return nil, err
 | 
						||
					}
 | 
						||
 | 
						||
					testkra, err := ListPreAuthKeysByUser(rx, 2) // testkra
 | 
						||
					if err != nil {
 | 
						||
						return nil, err
 | 
						||
					}
 | 
						||
 | 
						||
					return append(kratest, testkra...), nil
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, keys, 5)
 | 
						||
 | 
						||
				// Create map for easier validation by ID
 | 
						||
				keysByID := make(map[uint64]*types.PreAuthKey)
 | 
						||
				for i := range keys {
 | 
						||
					keysByID[keys[i].ID] = &keys[i]
 | 
						||
				}
 | 
						||
 | 
						||
				// Validate specific pre-auth key data and tag migration from pre_auth_key_acl_tags table
 | 
						||
				key1 := keysByID[1]
 | 
						||
				key2 := keysByID[2]
 | 
						||
				key3 := keysByID[3]
 | 
						||
				key4 := keysByID[4]
 | 
						||
				key5 := keysByID[5]
 | 
						||
 | 
						||
				require.NotNil(t, key1, "pre_auth_key 1 should exist")
 | 
						||
				require.NotNil(t, key2, "pre_auth_key 2 should exist")
 | 
						||
				require.NotNil(t, key3, "pre_auth_key 3 should exist")
 | 
						||
				require.NotNil(t, key4, "pre_auth_key 4 should exist")
 | 
						||
				require.NotNil(t, key5, "pre_auth_key 5 should exist")
 | 
						||
 | 
						||
				// Validate specific pre-auth key data and tag migration using cmp.Diff
 | 
						||
				expectedKeys := []types.PreAuthKey{
 | 
						||
					{
 | 
						||
						ID:     1,
 | 
						||
						Key:    "09b28f8c3351984874d46dace0a70177a8721933a950b663",
 | 
						||
						UserID: kratest.ID,
 | 
						||
						Tags:   []string{"tag:derp"},
 | 
						||
					},
 | 
						||
					{
 | 
						||
						ID:     2,
 | 
						||
						Key:    "3112b953cb344191b2d5aec1b891250125bf7b437eac5d26",
 | 
						||
						UserID: kratest.ID,
 | 
						||
						Tags:   []string{"tag:derp"},
 | 
						||
					},
 | 
						||
					{
 | 
						||
						ID:     3,
 | 
						||
						Key:    "7c23b9f215961e7609527aef78bf82fb19064b002d78c36f",
 | 
						||
						UserID: kratest.ID,
 | 
						||
						Tags:   []string{"tag:derp", "tag:merp"},
 | 
						||
					},
 | 
						||
					{
 | 
						||
						ID:     4,
 | 
						||
						Key:    "f2015583852b725220cc4b107fb288a4cf7ac259bd458a32",
 | 
						||
						UserID: testkra.ID,
 | 
						||
						Tags:   []string{"tag:test"},
 | 
						||
					},
 | 
						||
					{
 | 
						||
						ID:     5,
 | 
						||
						Key:    "b212b990165e897944dd3772786544402729fb349da50f57",
 | 
						||
						UserID: testkra.ID,
 | 
						||
						Tags:   []string{"tag:test", "tag:woop", "tag:dedu"},
 | 
						||
					},
 | 
						||
				}
 | 
						||
 | 
						||
				if diff := cmp.Diff(expectedKeys, keys, cmp.Comparer(func(a, b []string) bool {
 | 
						||
					slices.Sort(a)
 | 
						||
					slices.Sort(b)
 | 
						||
					return slices.Equal(a, b)
 | 
						||
				}), cmpopts.IgnoreFields(types.PreAuthKey{}, "User", "CreatedAt", "Reusable", "Ephemeral", "Used", "Expiration")); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() pre-auth key tags migration mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Verify pre_auth_key_acl_tags table was dropped after migration
 | 
						||
				if hsdb.DB.Migrator().HasTable("pre_auth_key_acl_tags") {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() table pre_auth_key_acl_tags should not exist after migration")
 | 
						||
				}
 | 
						||
			},
 | 
						||
		},
 | 
						||
		{
 | 
						||
			dbPath: "testdata/sqlite/0-23-0-to-0-24-0-no-more-special-types_dump.sql",
 | 
						||
			wantFunc: func(t *testing.T, hsdb *HSDatabase) {
 | 
						||
				t.Helper()
 | 
						||
				// Comprehensive data preservation validation for special types removal migration
 | 
						||
				// Expected data from dump: 2 users, 2 pre_auth_keys, 12 nodes
 | 
						||
 | 
						||
				// Verify users data preservation
 | 
						||
				users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
 | 
						||
					return ListUsers(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, users, 2, "should preserve all 2 users from original schema")
 | 
						||
 | 
						||
				// Verify pre_auth_keys data preservation
 | 
						||
				preAuthKeys, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
 | 
						||
					var keys []types.PreAuthKey
 | 
						||
					err := rx.Find(&keys).Error
 | 
						||
					return keys, err
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, preAuthKeys, 2, "should preserve all 2 pre_auth_keys from original schema")
 | 
						||
 | 
						||
				// Verify nodes data preservation and field validation
 | 
						||
				nodes, err := Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
 | 
						||
					return ListNodes(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, nodes, 12, "should preserve all 12 nodes from original schema")
 | 
						||
 | 
						||
				for _, node := range nodes {
 | 
						||
					assert.Falsef(t, node.MachineKey.IsZero(), "expected non zero machinekey")
 | 
						||
					assert.Contains(t, node.MachineKey.String(), "mkey:")
 | 
						||
					assert.Falsef(t, node.NodeKey.IsZero(), "expected non zero nodekey")
 | 
						||
					assert.Contains(t, node.NodeKey.String(), "nodekey:")
 | 
						||
					assert.Falsef(t, node.DiscoKey.IsZero(), "expected non zero discokey")
 | 
						||
					assert.Contains(t, node.DiscoKey.String(), "discokey:")
 | 
						||
					assert.NotNil(t, node.IPv4)
 | 
						||
					assert.NotNil(t, node.IPv6)
 | 
						||
					assert.Len(t, node.Endpoints, 1)
 | 
						||
					assert.NotNil(t, node.Hostinfo)
 | 
						||
					assert.NotNil(t, node.MachineKey)
 | 
						||
				}
 | 
						||
			},
 | 
						||
		},
 | 
						||
		{
 | 
						||
			dbPath: "testdata/sqlite/failing-node-preauth-constraint_dump.sql",
 | 
						||
			wantFunc: func(t *testing.T, hsdb *HSDatabase) {
 | 
						||
				t.Helper()
 | 
						||
				// Comprehensive data preservation validation for node-preauth constraint issue
 | 
						||
				// Expected data from dump: 1 user, 2 api_keys, 6 nodes
 | 
						||
 | 
						||
				// Verify users data preservation
 | 
						||
				users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
 | 
						||
					return ListUsers(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, users, 1, "should preserve all 1 user from original schema")
 | 
						||
 | 
						||
				// Verify api_keys data preservation
 | 
						||
				var apiKeyCount int
 | 
						||
				err = hsdb.DB.Raw("SELECT COUNT(*) FROM api_keys").Scan(&apiKeyCount).Error
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Equal(t, 2, apiKeyCount, "should preserve all 2 api_keys from original schema")
 | 
						||
 | 
						||
				// Verify nodes data preservation and field validation
 | 
						||
				nodes, err := Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
 | 
						||
					return ListNodes(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, nodes, 6, "should preserve all 6 nodes from original schema")
 | 
						||
 | 
						||
				for _, node := range nodes {
 | 
						||
					assert.Falsef(t, node.MachineKey.IsZero(), "expected non zero machinekey")
 | 
						||
					assert.Contains(t, node.MachineKey.String(), "mkey:")
 | 
						||
					assert.Falsef(t, node.NodeKey.IsZero(), "expected non zero nodekey")
 | 
						||
					assert.Contains(t, node.NodeKey.String(), "nodekey:")
 | 
						||
					assert.Falsef(t, node.DiscoKey.IsZero(), "expected non zero discokey")
 | 
						||
					assert.Contains(t, node.DiscoKey.String(), "discokey:")
 | 
						||
					assert.Nil(t, node.AuthKey)
 | 
						||
					assert.Nil(t, node.AuthKeyID)
 | 
						||
				}
 | 
						||
			},
 | 
						||
		},
 | 
						||
		{
 | 
						||
			dbPath: "testdata/sqlite/wrongly-migrated-schema-0.25.1_dump.sql",
 | 
						||
			wantFunc: func(t *testing.T, hsdb *HSDatabase) {
 | 
						||
				t.Helper()
 | 
						||
				// Test migration of a database that was wrongly migrated in 0.25.1
 | 
						||
				// This database has several issues:
 | 
						||
				// 1. Missing proper user unique constraints (idx_provider_identifier, idx_name_provider_identifier, idx_name_no_provider_identifier)
 | 
						||
				// 2. Still has routes table that should have been migrated to node.approved_routes
 | 
						||
				// 3. Wrong FOREIGN KEY constraint on pre_auth_keys (CASCADE instead of SET NULL)
 | 
						||
				// 4. Missing some required indexes
 | 
						||
 | 
						||
				// Verify users table data is preserved with specific user data
 | 
						||
				users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
 | 
						||
					return ListUsers(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, users, 2, "should preserve existing users")
 | 
						||
 | 
						||
				// Validate specific user data from dump file using cmp.Diff
 | 
						||
				expectedUsers := []types.User{
 | 
						||
					{Model: gorm.Model{ID: 1}, Name: "user2"},
 | 
						||
					{Model: gorm.Model{ID: 2}, Name: "user1"},
 | 
						||
				}
 | 
						||
 | 
						||
				if diff := cmp.Diff(expectedUsers, users,
 | 
						||
					cmpopts.IgnoreFields(types.User{}, "CreatedAt", "UpdatedAt", "DeletedAt", "DisplayName", "Email", "ProviderIdentifier", "Provider", "ProfilePicURL")); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() users mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Create maps for easier access in later validations
 | 
						||
				usersByName := make(map[string]*types.User)
 | 
						||
				for i := range users {
 | 
						||
					usersByName[users[i].Name] = &users[i]
 | 
						||
				}
 | 
						||
				user1 := usersByName["user1"]
 | 
						||
				user2 := usersByName["user2"]
 | 
						||
 | 
						||
				// Verify nodes table data is preserved and routes migrated to approved_routes
 | 
						||
				nodes, err := Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
 | 
						||
					return ListNodes(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, nodes, 3, "should preserve existing nodes")
 | 
						||
 | 
						||
				// Validate specific node data from dump file
 | 
						||
				nodesByID := make(map[uint64]*types.Node)
 | 
						||
				for i := range nodes {
 | 
						||
					nodesByID[nodes[i].ID.Uint64()] = nodes[i]
 | 
						||
				}
 | 
						||
 | 
						||
				node1 := nodesByID[1]
 | 
						||
				node2 := nodesByID[2]
 | 
						||
				node3 := nodesByID[3]
 | 
						||
				require.NotNil(t, node1, "node 1 should exist")
 | 
						||
				require.NotNil(t, node2, "node 2 should exist")
 | 
						||
				require.NotNil(t, node3, "node 3 should exist")
 | 
						||
 | 
						||
				// Validate specific node field data using cmp.Diff
 | 
						||
				expectedNodes := map[uint64]struct {
 | 
						||
					Hostname  string
 | 
						||
					GivenName string
 | 
						||
					IPv4      string
 | 
						||
					IPv6      string
 | 
						||
					UserID    uint
 | 
						||
				}{
 | 
						||
					1: {Hostname: "node1", GivenName: "node1", IPv4: "100.64.0.1", IPv6: "fd7a:115c:a1e0::1", UserID: user2.ID},
 | 
						||
					2: {Hostname: "node2", GivenName: "node2", IPv4: "100.64.0.2", IPv6: "fd7a:115c:a1e0::2", UserID: user2.ID},
 | 
						||
					3: {Hostname: "node3", GivenName: "node3", IPv4: "100.64.0.3", IPv6: "fd7a:115c:a1e0::3", UserID: user1.ID},
 | 
						||
				}
 | 
						||
 | 
						||
				for nodeID, expected := range expectedNodes {
 | 
						||
					node := nodesByID[nodeID]
 | 
						||
					require.NotNil(t, node, "node %d should exist", nodeID)
 | 
						||
 | 
						||
					actual := struct {
 | 
						||
						Hostname  string
 | 
						||
						GivenName string
 | 
						||
						IPv4      string
 | 
						||
						IPv6      string
 | 
						||
						UserID    uint
 | 
						||
					}{
 | 
						||
						Hostname:  node.Hostname,
 | 
						||
						GivenName: node.GivenName,
 | 
						||
						IPv4:      node.IPv4.String(),
 | 
						||
						IPv6: func() string {
 | 
						||
							if node.IPv6 != nil {
 | 
						||
								return node.IPv6.String()
 | 
						||
							} else {
 | 
						||
								return ""
 | 
						||
							}
 | 
						||
						}(),
 | 
						||
						UserID: node.UserID,
 | 
						||
					}
 | 
						||
 | 
						||
					if diff := cmp.Diff(expected, actual); diff != "" {
 | 
						||
						t.Errorf("TestSQLiteMigrationAndDataValidation() node %d basic fields mismatch (-want +got):\n%s", nodeID, diff)
 | 
						||
					}
 | 
						||
 | 
						||
					// Special validation for MachineKey content for node 1 only
 | 
						||
					if nodeID == 1 {
 | 
						||
						assert.Contains(t, node.MachineKey.String(), "mkey:1efe4388236c1c83fe0a19d3ce7c321ab81e138a4da57917c231ce4c01944409")
 | 
						||
					}
 | 
						||
				}
 | 
						||
 | 
						||
				// Check that routes were migrated from routes table to node.approved_routes using cmp.Diff
 | 
						||
				// Original routes table had 4 routes for nodes 1, 2, 3:
 | 
						||
				// Node 1: 0.0.0.0/0 (enabled), ::/0 (enabled) -> should have 2 approved routes
 | 
						||
				// Node 2: 192.168.100.0/24 (enabled) -> should have 1 approved route
 | 
						||
				// Node 3: 10.0.0.0/8 (disabled) -> should have 0 approved routes
 | 
						||
				expectedRoutes := map[uint64][]netip.Prefix{
 | 
						||
					1: {netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")},
 | 
						||
					2: {netip.MustParsePrefix("192.168.100.0/24")},
 | 
						||
					3: nil,
 | 
						||
				}
 | 
						||
 | 
						||
				actualRoutes := map[uint64][]netip.Prefix{
 | 
						||
					1: node1.ApprovedRoutes,
 | 
						||
					2: node2.ApprovedRoutes,
 | 
						||
					3: node3.ApprovedRoutes,
 | 
						||
				}
 | 
						||
 | 
						||
				if diff := cmp.Diff(expectedRoutes, actualRoutes, util.PrefixComparer); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() routes migration mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Verify pre_auth_keys data is preserved with specific key data
 | 
						||
				preAuthKeys, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) {
 | 
						||
					var keys []types.PreAuthKey
 | 
						||
					err := rx.Find(&keys).Error
 | 
						||
					return keys, err
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, preAuthKeys, 2, "should preserve existing pre_auth_keys")
 | 
						||
 | 
						||
				// Validate specific pre_auth_key data from dump file using cmp.Diff
 | 
						||
				expectedKeys := []types.PreAuthKey{
 | 
						||
					{
 | 
						||
						ID:       1,
 | 
						||
						Key:      "3d133ec953e31fd41edbd935371234f762b4bae300cea618",
 | 
						||
						UserID:   user2.ID,
 | 
						||
						Reusable: true,
 | 
						||
						Used:     true,
 | 
						||
					},
 | 
						||
					{
 | 
						||
						ID:       2,
 | 
						||
						Key:      "9813cc1df1832259fb6322dad788bb9bec89d8a01eef683a",
 | 
						||
						UserID:   user1.ID,
 | 
						||
						Reusable: true,
 | 
						||
						Used:     true,
 | 
						||
					},
 | 
						||
				}
 | 
						||
 | 
						||
				if diff := cmp.Diff(expectedKeys, preAuthKeys,
 | 
						||
					cmpopts.IgnoreFields(types.PreAuthKey{}, "User", "CreatedAt", "Expiration", "Ephemeral", "Tags")); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() pre_auth_keys mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Verify api_keys data is preserved with specific key data
 | 
						||
				var apiKeys []struct {
 | 
						||
					ID         uint64
 | 
						||
					Prefix     string
 | 
						||
					Hash       []byte
 | 
						||
					CreatedAt  string
 | 
						||
					Expiration string
 | 
						||
					LastSeen   string
 | 
						||
				}
 | 
						||
				err = hsdb.DB.Raw("SELECT id, prefix, hash, created_at, expiration, last_seen FROM api_keys").Scan(&apiKeys).Error
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Len(t, apiKeys, 1, "should preserve existing api_keys")
 | 
						||
 | 
						||
				// Validate specific api_key data from dump file using cmp.Diff
 | 
						||
				expectedAPIKey := struct {
 | 
						||
					ID     uint64
 | 
						||
					Prefix string
 | 
						||
					Hash   []byte
 | 
						||
				}{
 | 
						||
					ID:     1,
 | 
						||
					Prefix: "ak_test",
 | 
						||
					Hash:   []byte{0xde, 0xad, 0xbe, 0xef},
 | 
						||
				}
 | 
						||
 | 
						||
				actualAPIKey := struct {
 | 
						||
					ID     uint64
 | 
						||
					Prefix string
 | 
						||
					Hash   []byte
 | 
						||
				}{
 | 
						||
					ID:     apiKeys[0].ID,
 | 
						||
					Prefix: apiKeys[0].Prefix,
 | 
						||
					Hash:   apiKeys[0].Hash,
 | 
						||
				}
 | 
						||
 | 
						||
				if diff := cmp.Diff(expectedAPIKey, actualAPIKey); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() api_key mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Validate date fields separately since they need Contains check
 | 
						||
				assert.Contains(t, apiKeys[0].CreatedAt, "2025-12-31", "created_at should be preserved")
 | 
						||
				assert.Contains(t, apiKeys[0].Expiration, "2025-06-18", "expiration should be preserved")
 | 
						||
 | 
						||
				// Verify that routes table no longer exists (should have been dropped)
 | 
						||
				var routesTableExists bool
 | 
						||
				err = hsdb.DB.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='routes'").Row().Scan(&routesTableExists)
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.False(t, routesTableExists, "routes table should have been dropped")
 | 
						||
 | 
						||
				// Verify all required indexes exist with correct structure using cmp.Diff
 | 
						||
				expectedIndexes := []string{
 | 
						||
					"idx_users_deleted_at",
 | 
						||
					"idx_provider_identifier",
 | 
						||
					"idx_name_provider_identifier",
 | 
						||
					"idx_name_no_provider_identifier",
 | 
						||
					"idx_api_keys_prefix",
 | 
						||
					"idx_policies_deleted_at",
 | 
						||
				}
 | 
						||
 | 
						||
				expectedIndexMap := make(map[string]bool)
 | 
						||
				for _, index := range expectedIndexes {
 | 
						||
					expectedIndexMap[index] = true
 | 
						||
				}
 | 
						||
 | 
						||
				actualIndexMap := make(map[string]bool)
 | 
						||
				for _, indexName := range expectedIndexes {
 | 
						||
					var indexExists bool
 | 
						||
					err = hsdb.DB.Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name=?", indexName).Row().Scan(&indexExists)
 | 
						||
					require.NoError(t, err)
 | 
						||
					actualIndexMap[indexName] = indexExists
 | 
						||
				}
 | 
						||
 | 
						||
				if diff := cmp.Diff(expectedIndexMap, actualIndexMap); diff != "" {
 | 
						||
					t.Errorf("TestSQLiteMigrationAndDataValidation() indexes existence mismatch (-want +got):\n%s", diff)
 | 
						||
				}
 | 
						||
 | 
						||
				// Verify proper foreign key constraints are set
 | 
						||
				// Check that pre_auth_keys has correct FK constraint (SET NULL, not CASCADE)
 | 
						||
				var preAuthKeyConstraint string
 | 
						||
				err = hsdb.DB.Raw("SELECT sql FROM sqlite_master WHERE type='table' AND name='pre_auth_keys'").Row().Scan(&preAuthKeyConstraint)
 | 
						||
				require.NoError(t, err)
 | 
						||
				assert.Contains(t, preAuthKeyConstraint, "ON DELETE SET NULL", "pre_auth_keys should have SET NULL constraint")
 | 
						||
				assert.NotContains(t, preAuthKeyConstraint, "ON DELETE CASCADE", "pre_auth_keys should not have CASCADE constraint")
 | 
						||
 | 
						||
				// Verify that user unique constraints work properly
 | 
						||
				// Try to create duplicate local user (should fail)
 | 
						||
				err = hsdb.DB.Create(&types.User{Name: users[0].Name}).Error
 | 
						||
				require.Error(t, err, "should not allow duplicate local usernames")
 | 
						||
				assert.Contains(t, err.Error(), "UNIQUE constraint", "should fail with unique constraint error")
 | 
						||
			},
 | 
						||
		},
 | 
						||
	}
 | 
						||
 | 
						||
	for _, tt := range tests {
 | 
						||
		t.Run(tt.dbPath, func(t *testing.T) {
 | 
						||
			if !strings.HasSuffix(tt.dbPath, ".sql") {
 | 
						||
				t.Fatalf("TestSQLiteMigrationAndDataValidation only supports .sql files, got: %s", tt.dbPath)
 | 
						||
			}
 | 
						||
 | 
						||
			hsdb := dbForTestWithPath(t, tt.dbPath)
 | 
						||
			if tt.wantFunc != nil {
 | 
						||
				tt.wantFunc(t, hsdb)
 | 
						||
			}
 | 
						||
		})
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
func emptyCache() *zcache.Cache[types.RegistrationID, types.RegisterNode] {
 | 
						||
	return zcache.New[types.RegistrationID, types.RegisterNode](time.Minute, time.Hour)
 | 
						||
}
 | 
						||
 | 
						||
func createSQLiteFromSQLFile(sqlFilePath, dbPath string) error {
 | 
						||
	db, err := sql.Open("sqlite", dbPath)
 | 
						||
	if err != nil {
 | 
						||
		return err
 | 
						||
	}
 | 
						||
	defer db.Close()
 | 
						||
 | 
						||
	schemaContent, err := os.ReadFile(sqlFilePath)
 | 
						||
	if err != nil {
 | 
						||
		return err
 | 
						||
	}
 | 
						||
 | 
						||
	_, err = db.Exec(string(schemaContent))
 | 
						||
 | 
						||
	return err
 | 
						||
}
 | 
						||
 | 
						||
// requireConstraintFailed checks if the error is a constraint failure with
 | 
						||
// either SQLite and PostgreSQL error messages.
 | 
						||
func requireConstraintFailed(t *testing.T, err error) {
 | 
						||
	t.Helper()
 | 
						||
	require.Error(t, err)
 | 
						||
	if !strings.Contains(err.Error(), "UNIQUE constraint failed:") && !strings.Contains(err.Error(), "violates unique constraint") {
 | 
						||
		require.Failf(t, "expected error to contain a constraint failure, got: %s", err.Error())
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
func TestConstraints(t *testing.T) {
 | 
						||
	tests := []struct {
 | 
						||
		name string
 | 
						||
		run  func(*testing.T, *gorm.DB)
 | 
						||
	}{
 | 
						||
		{
 | 
						||
			name: "no-duplicate-username-if-no-oidc",
 | 
						||
			run: func(t *testing.T, db *gorm.DB) {
 | 
						||
				_, err := CreateUser(db, types.User{Name: "user1"})
 | 
						||
				require.NoError(t, err)
 | 
						||
				_, err = CreateUser(db, types.User{Name: "user1"})
 | 
						||
				requireConstraintFailed(t, err)
 | 
						||
			},
 | 
						||
		},
 | 
						||
		{
 | 
						||
			name: "no-oidc-duplicate-username-and-id",
 | 
						||
			run: func(t *testing.T, db *gorm.DB) {
 | 
						||
				user := types.User{
 | 
						||
					Model: gorm.Model{ID: 1},
 | 
						||
					Name:  "user1",
 | 
						||
				}
 | 
						||
				user.ProviderIdentifier = sql.NullString{String: "http://test.com/user1", Valid: true}
 | 
						||
 | 
						||
				err := db.Save(&user).Error
 | 
						||
				require.NoError(t, err)
 | 
						||
 | 
						||
				user = types.User{
 | 
						||
					Model: gorm.Model{ID: 2},
 | 
						||
					Name:  "user1",
 | 
						||
				}
 | 
						||
				user.ProviderIdentifier = sql.NullString{String: "http://test.com/user1", Valid: true}
 | 
						||
 | 
						||
				err = db.Save(&user).Error
 | 
						||
				requireConstraintFailed(t, err)
 | 
						||
			},
 | 
						||
		},
 | 
						||
		{
 | 
						||
			name: "no-oidc-duplicate-id",
 | 
						||
			run: func(t *testing.T, db *gorm.DB) {
 | 
						||
				user := types.User{
 | 
						||
					Model: gorm.Model{ID: 1},
 | 
						||
					Name:  "user1",
 | 
						||
				}
 | 
						||
				user.ProviderIdentifier = sql.NullString{String: "http://test.com/user1", Valid: true}
 | 
						||
 | 
						||
				err := db.Save(&user).Error
 | 
						||
				require.NoError(t, err)
 | 
						||
 | 
						||
				user = types.User{
 | 
						||
					Model: gorm.Model{ID: 2},
 | 
						||
					Name:  "user1.1",
 | 
						||
				}
 | 
						||
				user.ProviderIdentifier = sql.NullString{String: "http://test.com/user1", Valid: true}
 | 
						||
 | 
						||
				err = db.Save(&user).Error
 | 
						||
				requireConstraintFailed(t, err)
 | 
						||
			},
 | 
						||
		},
 | 
						||
		{
 | 
						||
			name: "allow-duplicate-username-cli-then-oidc",
 | 
						||
			run: func(t *testing.T, db *gorm.DB) {
 | 
						||
				_, err := CreateUser(db, types.User{Name: "user1"}) // Create CLI username
 | 
						||
				require.NoError(t, err)
 | 
						||
 | 
						||
				user := types.User{
 | 
						||
					Name:               "user1",
 | 
						||
					ProviderIdentifier: sql.NullString{String: "http://test.com/user1", Valid: true},
 | 
						||
				}
 | 
						||
 | 
						||
				err = db.Save(&user).Error
 | 
						||
				require.NoError(t, err)
 | 
						||
			},
 | 
						||
		},
 | 
						||
		{
 | 
						||
			name: "allow-duplicate-username-oidc-then-cli",
 | 
						||
			run: func(t *testing.T, db *gorm.DB) {
 | 
						||
				user := types.User{
 | 
						||
					Name:               "user1",
 | 
						||
					ProviderIdentifier: sql.NullString{String: "http://test.com/user1", Valid: true},
 | 
						||
				}
 | 
						||
 | 
						||
				err := db.Save(&user).Error
 | 
						||
				require.NoError(t, err)
 | 
						||
 | 
						||
				_, err = CreateUser(db, types.User{Name: "user1"}) // Create CLI username
 | 
						||
				require.NoError(t, err)
 | 
						||
			},
 | 
						||
		},
 | 
						||
	}
 | 
						||
 | 
						||
	for _, tt := range tests {
 | 
						||
		t.Run(tt.name+"-postgres", func(t *testing.T) {
 | 
						||
			db := newPostgresTestDB(t)
 | 
						||
			tt.run(t, db.DB.Debug())
 | 
						||
		})
 | 
						||
		t.Run(tt.name+"-sqlite", func(t *testing.T) {
 | 
						||
			db, err := newSQLiteTestDB()
 | 
						||
			if err != nil {
 | 
						||
				t.Fatalf("creating database: %s", err)
 | 
						||
			}
 | 
						||
 | 
						||
			tt.run(t, db.DB.Debug())
 | 
						||
		})
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
// TestPostgresMigrationAndDataValidation tests specific PostgreSQL migration scenarios
 | 
						||
// and validates data integrity after migration. All migrations that require data validation
 | 
						||
// should be added here.
 | 
						||
//
 | 
						||
// TODO(kradalby): Convert to use plain text SQL dumps instead of binary .pssql dumps for consistency
 | 
						||
// with SQLite tests and easier version control.
 | 
						||
func TestPostgresMigrationAndDataValidation(t *testing.T) {
 | 
						||
	tests := []struct {
 | 
						||
		name     string
 | 
						||
		dbPath   string
 | 
						||
		wantFunc func(*testing.T, *HSDatabase)
 | 
						||
	}{
 | 
						||
		{
 | 
						||
			name:   "user-idx-breaking",
 | 
						||
			dbPath: "testdata/postgres/pre-24-postgresdb.pssql.dump",
 | 
						||
			wantFunc: func(t *testing.T, hsdb *HSDatabase) {
 | 
						||
				t.Helper()
 | 
						||
				users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
 | 
						||
					return ListUsers(rx)
 | 
						||
				})
 | 
						||
				require.NoError(t, err)
 | 
						||
 | 
						||
				for _, user := range users {
 | 
						||
					assert.NotEmpty(t, user.Name)
 | 
						||
					assert.Empty(t, user.ProfilePicURL)
 | 
						||
					assert.Empty(t, user.Email)
 | 
						||
				}
 | 
						||
			},
 | 
						||
		},
 | 
						||
	}
 | 
						||
 | 
						||
	for _, tt := range tests {
 | 
						||
		t.Run(tt.name, func(t *testing.T) {
 | 
						||
			u := newPostgresDBForTest(t)
 | 
						||
 | 
						||
			pgRestorePath, err := exec.LookPath("pg_restore")
 | 
						||
			if err != nil {
 | 
						||
				t.Fatal("pg_restore not found in PATH. Please install it and ensure it is accessible.")
 | 
						||
			}
 | 
						||
 | 
						||
			// Construct the pg_restore command
 | 
						||
			cmd := exec.Command(pgRestorePath, "--verbose", "--if-exists", "--clean", "--no-owner", "--dbname", u.String(), tt.dbPath)
 | 
						||
 | 
						||
			// Set the output streams
 | 
						||
			cmd.Stdout = os.Stdout
 | 
						||
			cmd.Stderr = os.Stderr
 | 
						||
 | 
						||
			// Execute the command
 | 
						||
			err = cmd.Run()
 | 
						||
			if err != nil {
 | 
						||
				t.Fatalf("failed to restore postgres database: %s", err)
 | 
						||
			}
 | 
						||
 | 
						||
			db = newHeadscaleDBFromPostgresURL(t, u)
 | 
						||
 | 
						||
			if tt.wantFunc != nil {
 | 
						||
				tt.wantFunc(t, db)
 | 
						||
			}
 | 
						||
		})
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
func dbForTest(t *testing.T) *HSDatabase {
 | 
						||
	t.Helper()
 | 
						||
	return dbForTestWithPath(t, "")
 | 
						||
}
 | 
						||
 | 
						||
func dbForTestWithPath(t *testing.T, sqlFilePath string) *HSDatabase {
 | 
						||
	t.Helper()
 | 
						||
 | 
						||
	dbPath := t.TempDir() + "/headscale_test.db"
 | 
						||
 | 
						||
	// If SQL file path provided, validate and create database from it
 | 
						||
	if sqlFilePath != "" {
 | 
						||
		// Validate that the file is a SQL text file
 | 
						||
		if !strings.HasSuffix(sqlFilePath, ".sql") {
 | 
						||
			t.Fatalf("dbForTestWithPath only accepts .sql files, got: %s", sqlFilePath)
 | 
						||
		}
 | 
						||
 | 
						||
		err := createSQLiteFromSQLFile(sqlFilePath, dbPath)
 | 
						||
		if err != nil {
 | 
						||
			t.Fatalf("setting up database from SQL file %s: %s", sqlFilePath, err)
 | 
						||
		}
 | 
						||
	}
 | 
						||
 | 
						||
	db, err := NewHeadscaleDatabase(
 | 
						||
		types.DatabaseConfig{
 | 
						||
			Type: "sqlite3",
 | 
						||
			Sqlite: types.SqliteConfig{
 | 
						||
				Path: dbPath,
 | 
						||
			},
 | 
						||
		},
 | 
						||
		"",
 | 
						||
		emptyCache(),
 | 
						||
	)
 | 
						||
	if err != nil {
 | 
						||
		t.Fatalf("setting up database: %s", err)
 | 
						||
	}
 | 
						||
 | 
						||
	if sqlFilePath != "" {
 | 
						||
		t.Logf("database set up from %s at: %s", sqlFilePath, dbPath)
 | 
						||
	} else {
 | 
						||
		t.Logf("database set up at: %s", dbPath)
 | 
						||
	}
 | 
						||
 | 
						||
	return db
 | 
						||
}
 | 
						||
 | 
						||
// TestSQLiteAllTestdataMigrations tests migration compatibility across all SQLite schemas
 | 
						||
// in the testdata directory. It verifies they can be successfully migrated to the current
 | 
						||
// schema version. This test only validates migration success, not data integrity.
 | 
						||
//
 | 
						||
// A lot of the schemas have been automatically generated with old Headscale binaries on empty databases
 | 
						||
// (no user/node data):
 | 
						||
// - `headscale_<VERSION>_schema.sql` (created with `sqlite3 headscale.db .schema`)
 | 
						||
// - `headscale_<VERSION>_dump.sql` (created with `sqlite3 headscale.db .dump`)
 | 
						||
// where `_dump.sql` contains the migration steps that have been applied to the database.
 | 
						||
func TestSQLiteAllTestdataMigrations(t *testing.T) {
 | 
						||
	t.Parallel()
 | 
						||
	schemas, err := os.ReadDir("testdata/sqlite")
 | 
						||
	require.NoError(t, err)
 | 
						||
 | 
						||
	t.Logf("loaded %d schemas", len(schemas))
 | 
						||
 | 
						||
	for _, schema := range schemas {
 | 
						||
		if schema.IsDir() {
 | 
						||
			continue
 | 
						||
		}
 | 
						||
 | 
						||
		t.Logf("validating: %s", schema.Name())
 | 
						||
 | 
						||
		t.Run(schema.Name(), func(t *testing.T) {
 | 
						||
			t.Parallel()
 | 
						||
 | 
						||
			dbPath := t.TempDir() + "/headscale_test.db"
 | 
						||
 | 
						||
			// Setup a database with the old schema
 | 
						||
			schemaPath := filepath.Join("testdata/sqlite", schema.Name())
 | 
						||
			err := createSQLiteFromSQLFile(schemaPath, dbPath)
 | 
						||
			require.NoError(t, err)
 | 
						||
 | 
						||
			_, err = NewHeadscaleDatabase(
 | 
						||
				types.DatabaseConfig{
 | 
						||
					Type: "sqlite3",
 | 
						||
					Sqlite: types.SqliteConfig{
 | 
						||
						Path: dbPath,
 | 
						||
					},
 | 
						||
				},
 | 
						||
				"",
 | 
						||
				emptyCache(),
 | 
						||
			)
 | 
						||
			require.NoError(t, err)
 | 
						||
		})
 | 
						||
	}
 | 
						||
}
 |