migrate_test.go raw
1 package main
2
3 import (
4 "fmt"
5 "os"
6 "strconv"
7 "testing"
8 "time"
9
10 "github.com/sirupsen/logrus"
11 "github.com/stretchr/testify/require"
12 "gorm.io/datatypes"
13 "gorm.io/gorm"
14
15 "github.com/getAlby/hub/db"
16 "github.com/getAlby/hub/logger"
17 test_db "github.com/getAlby/hub/tests/db"
18 )
19
20 type testEnvironment struct {
21 source *gorm.DB
22 dest *gorm.DB
23 }
24
25 func (e *testEnvironment) cleanup(t *testing.T) {
26 err := db.Stop(e.source)
27 require.NoError(t, err)
28
29 err = db.Stop(e.dest)
30 require.NoError(t, err)
31 }
32
33 func TestMigrate(t *testing.T) {
34 type testCase struct {
35 name string
36 sourceURI string
37 destURI string
38 }
39
40 // Test migration between sqlite instances for basic sanity checking.
41 tc := []testCase{
42 {
43 name: "sqlite to sqlite",
44 sourceURI: getTestSqliteURI(0),
45 destURI: getTestSqliteURI(1),
46 },
47 }
48
49 // Only run Postgres tests if Postgres is configured and its URI is set.
50 if getTestPostgresURI() != "" {
51 tcPg := []testCase{
52 {
53 name: "sqlite to postgres",
54 sourceURI: getTestSqliteURI(0),
55 destURI: getTestPostgresURI(),
56 },
57 {
58 name: "postgres to sqlite",
59 sourceURI: getTestPostgresURI(),
60 destURI: getTestSqliteURI(0),
61 },
62 }
63
64 tc = append(tc, tcPg...)
65 }
66
67 for _, tt := range tc {
68 t.Run(tt.name, func(t *testing.T) {
69 env, err := setupTest(t, tt.sourceURI, tt.destURI)
70 require.NoError(t, err)
71 defer env.cleanup(t)
72
73 err = db.MigrateDB(env.source, env.dest)
74 require.NoError(t, err)
75
76 requireCount[db.App](t, env.dest, 2)
77 requireCount[db.AppPermission](t, env.dest, 2)
78 requireCount[db.RequestEvent](t, env.dest, 1)
79 requireCount[db.ResponseEvent](t, env.dest, 1)
80 requireCount[db.Transaction](t, env.dest, 1)
81 requireCount[db.Swap](t, env.dest, 1)
82 requireCount[db.Forward](t, env.dest, 1)
83 requireCount[db.UserConfig](t, env.dest, 1)
84 })
85 }
86 }
87
88 func getTestSqliteURI(dbIndex int) string {
89 if uri := os.Getenv("TEST_DB_MIGRATE_SQLITE_URI"); uri != "" {
90 return uri
91 }
92
93 return fmt.Sprintf("file:testmemdb%d?mode=memory&cache=shared&_txlock=immediate&_foreign_keys=1", dbIndex)
94 }
95
96 func getTestPostgresURI() string {
97 return os.Getenv("TEST_DB_MIGRATE_POSTGRES_URI")
98 }
99
100 func setupTest(t *testing.T, sourceURI string, destURI string) (*testEnvironment, error) {
101 logger.Init(strconv.Itoa(int(logrus.DebugLevel)))
102
103 source, err := test_db.NewDBWithURI(t, sourceURI)
104 if err != nil {
105 t.Fatalf("failed to open source database: %v", err)
106 }
107
108 dest, err := test_db.NewDBWithURI(t, destURI)
109 if err != nil {
110 t.Fatalf("failed to open destination database: %v", err)
111 }
112
113 insertMockData(t, source)
114
115 return &testEnvironment{
116 source: source,
117 dest: dest,
118 }, nil
119 }
120
121 func insertMockData(t *testing.T, tx *gorm.DB) {
122 baseTime := time.Date(2025, 01, 15, 8, 0, 0, 0, time.UTC)
123
124 userCfg1 := &db.UserConfig{
125 Key: "Relay",
126 Value: "wss://relay.getalby.com",
127 Encrypted: false,
128 CreatedAt: baseTime,
129 UpdatedAt: baseTime,
130 }
131 create(t, tx, userCfg1)
132
133 app1 := &db.App{
134 Name: "test1",
135 Description: "test1 description",
136 AppPubkey: "2b7dea2866958f17c568cf024e113db7a3baa9c253a9016889196b8d0b11c7ae",
137 WalletPubkey: ptr("f766024546ddbdc45db6016714047e34117d5e0d68e51fae06ffca9687783995"),
138 CreatedAt: baseTime,
139 UpdatedAt: baseTime,
140 Isolated: false,
141 Metadata: datatypes.JSON("{}"),
142 }
143 create(t, tx, app1)
144
145 app1Perm := &db.AppPermission{
146 App: *app1,
147 Scope: "pay_invoice",
148 MaxAmountSat: 0,
149 BudgetRenewal: "monthly",
150 ExpiresAt: nil,
151 CreatedAt: baseTime,
152 UpdatedAt: baseTime,
153 }
154 create(t, tx, app1Perm)
155
156 app2 := &db.App{
157 Name: "test2",
158 Description: "test2 description",
159 AppPubkey: "560f31e764f7af64719aba1dfdc0bcb3e681d48bb76265ca939622e1a719fe2a",
160 WalletPubkey: ptr("b44c5b3e9c3105b9347cce9f4bbfc899df13c591976fe0f706c1aacd4358020b"),
161 CreatedAt: baseTime,
162 UpdatedAt: baseTime,
163 Isolated: false,
164 Metadata: datatypes.JSON("{}"),
165 }
166 create(t, tx, app2)
167
168 app2Perm := &db.AppPermission{
169 App: *app2,
170 Scope: "get_info",
171 MaxAmountSat: 0,
172 BudgetRenewal: "monthly",
173 ExpiresAt: nil,
174 CreatedAt: baseTime,
175 UpdatedAt: baseTime,
176 }
177 create(t, tx, app2Perm)
178
179 requestEvent1 := &db.RequestEvent{
180 AppId: &app1.ID,
181 NostrId: "a35a1ca6d1a06e08a509f2c8fe3edb2ba10811d030e2f6f3239e9f21203ac954",
182 ContentData: "{}",
183 Method: "pay_invoice",
184 State: "executed",
185 CreatedAt: baseTime,
186 UpdatedAt: baseTime,
187 }
188 create(t, tx, requestEvent1)
189
190 responseEvent1 := &db.ResponseEvent{
191 NostrId: "e30d55d0e4f0d5391a1a1379f1d8b7d38ad02b3554b06ca993aa8790a3153f61",
192 RequestId: requestEvent1.ID,
193 State: "confirmed",
194 RepliedAt: baseTime,
195 CreatedAt: baseTime,
196 UpdatedAt: baseTime,
197 }
198 create(t, tx, responseEvent1)
199
200 transaction1 := &db.Transaction{
201 AppId: &app1.ID,
202 RequestEventId: &requestEvent1.ID,
203 Type: "outgoing",
204 State: "settled",
205 AmountMsat: 21000,
206 FeeMsat: 1000,
207 PaymentRequest: "lnbc210n1invoice",
208 PaymentHash: "13d9764a54269fa4d5f4e7c410f4ffdbc839bbeaa2fcbb96343ca502f0c86e34",
209 Description: "test transaction",
210 Preimage: ptr("2c1ee1b464b1a1a147debe0ac0c8ce4b615f9bfa64d12a25c1c4d10ea45a5b02"),
211 CreatedAt: baseTime,
212 UpdatedAt: baseTime,
213 SettledAt: &baseTime,
214 Metadata: datatypes.JSON("{}"),
215 Boostagram: datatypes.JSON("{}"),
216 }
217 create(t, tx, transaction1)
218
219 swap1 := &db.Swap{
220 SwapId: "swap1",
221 Type: "out",
222 State: "success",
223 Invoice: "lnbc210n1swapinvoice",
224 SendAmountSat: 21000,
225 ReceiveAmountSat: 20000,
226 Preimage: "35a3f1a7a06a41b9ba3a1b1a8ff852e5085b3b593f8ba4677a35a1ca6d1a06e0",
227 PaymentHash: "e6b1a1379f1d8b7d38ad02b3554b06ca993aa8790a3153f61e30d55d0e4f0d53",
228 DestinationAddress: "bc1qtest",
229 LockupAddress: "bc1qlockup",
230 LockupTxId: "lockuptx",
231 ClaimTxId: "claimtx",
232 AutoSwap: false,
233 TimeoutBlockHeight: 900000,
234 BoltzPubkey: "02d1a06e08a509f2c8fe3edb2ba10811d030e2f6f3239e9f21203ac954a35a1c",
235 SwapTree: datatypes.JSON("{}"),
236 CreatedAt: baseTime,
237 UpdatedAt: baseTime,
238 }
239 create(t, tx, swap1)
240
241 forward1 := &db.Forward{
242 OutboundAmountForwardedMsat: 1000000,
243 TotalFeeEarnedMsat: 1000,
244 CreatedAt: baseTime,
245 UpdatedAt: baseTime,
246 }
247 create(t, tx, forward1)
248 }
249
250 func requireCount[T any](t *testing.T, tx *gorm.DB, expected int64) {
251 var count int64
252 var model T
253 require.NoError(t, tx.Model(&model).Count(&count).Error)
254 require.Equal(t, expected, count)
255 }
256
257 func create[T any](t *testing.T, tx *gorm.DB, v T) *gorm.DB {
258 tx.Create(v)
259 require.NoError(t, tx.Error)
260 return tx
261 }
262
263 func ptr[T any](v T) *T {
264 return &v
265 }
266