Skip to content

Commit 7f6fa9d

Browse files
committed
add database tests and remove unused constants
1 parent 2465d22 commit 7f6fa9d

10 files changed

Lines changed: 341 additions & 30 deletions

File tree

__test__/unit/models/deliveryPlatformKind.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,5 @@ describe('DeliveryPlatformKind', () => {
55
expect(DeliveryPlatformKind.ChromeLike).toBe(5);
66
expect(DeliveryPlatformKind.SafariLegacy).toBe(7);
77
expect(DeliveryPlatformKind.Firefox).toBe(8);
8-
expect(DeliveryPlatformKind.Email).toBe(11);
9-
expect(DeliveryPlatformKind.Edge).toBe(12);
10-
expect(DeliveryPlatformKind.SMS).toBe(14);
11-
expect(DeliveryPlatformKind.SafariVapid).toBe(17);
128
});
139
});

src/shared/config/constants.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,5 @@ export const NotificationClickActionBehavior = {
1111
export const ConfigIntegrationKind = {
1212
TypicalSite: 'typical',
1313
WordPress: 'wordpress',
14-
Shopify: 'shopify',
15-
Blogger: 'blogger',
16-
Magento: 'magento',
17-
Drupal: 'drupal',
18-
SquareSpace: 'squarespace',
19-
Joomla: 'joomla',
20-
Weebly: 'weebly',
21-
Wix: 'wix',
2214
Custom: 'custom',
2315
} as const;

src/shared/database/client.test.ts

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
import {
2+
APP_ID,
3+
DUMMY_EXTERNAL_ID,
4+
DUMMY_ONESIGNAL_ID,
5+
} from '__test__/constants';
6+
import { deleteDB, type IDBPDatabase } from 'idb';
7+
import { SubscriptionType } from '../subscriptions/constants';
8+
import { closeDb, getDb } from './client';
9+
import { DATABASE_NAME, LegacyModelName, ModelName } from './constants';
10+
import type { IndexedDBSchema } from './types';
11+
12+
vi.useRealTimers;
13+
14+
beforeEach(async () => {
15+
await closeDb();
16+
await deleteDB(DATABASE_NAME);
17+
});
18+
19+
describe('general', () => {
20+
const values: IndexedDBSchema['Options']['value'][] = [
21+
{ key: 'consentGiven', value: true },
22+
{ key: 'defaultIcon', value: 'icon' },
23+
{ key: 'defaultTitle', value: 'title' },
24+
];
25+
26+
test('can get 1 or all values', async () => {
27+
const db = await getDb();
28+
for (const value of values) {
29+
await db.put('Options', value);
30+
}
31+
32+
const retrievedValue = await db.get('Options', 'consentGiven');
33+
expect(retrievedValue).toEqual({
34+
key: 'consentGiven',
35+
value: true,
36+
});
37+
38+
const retrievedValues = await db.getAll('Options');
39+
expect(retrievedValues).toEqual(values);
40+
});
41+
42+
test('can set/update a value', async () => {
43+
const db = await getDb();
44+
await db.put('Options', { key: 'consentGiven', value: 'optionsValue' });
45+
const retrievedValue = await db.get('Options', 'consentGiven');
46+
expect(retrievedValue).toEqual({
47+
key: 'consentGiven',
48+
value: 'optionsValue',
49+
});
50+
51+
// can update value
52+
await db.put('Options', { key: 'consentGiven', value: 'optionsValue2' });
53+
const retrievedValue2 = await db.get('Options', 'consentGiven');
54+
expect(retrievedValue2).toEqual({
55+
key: 'consentGiven',
56+
value: 'optionsValue2',
57+
});
58+
59+
await expect(
60+
// @ts-expect-error - for testing invalid value
61+
db.put('Options', ''),
62+
).rejects.toThrow(
63+
'Data provided to an operation does not meet requirements.',
64+
);
65+
});
66+
67+
test('can remove a value', async () => {
68+
const db = await getDb();
69+
for (const value of values) {
70+
await db.put('Options', value);
71+
}
72+
73+
// can remove a single value
74+
await db.delete('Options', 'consentGiven');
75+
const retrievedValue = await db.get('Options', 'consentGiven');
76+
expect(retrievedValue).toBeUndefined();
77+
78+
// can remove remaining values
79+
await db.clear('Options');
80+
const retrievedValues = await db.getAll('Options');
81+
expect(retrievedValues).toEqual([]);
82+
83+
// resolves undefined if key does not exist
84+
await expect(
85+
// @ts-expect-error - using invalid key for testing
86+
db.delete('Options', 'non-existent-key'),
87+
).resolves.toBeUndefined();
88+
});
89+
});
90+
91+
describe('migrations', () => {
92+
describe('v5', () => {
93+
test('can to write to new v5 tables', async () => {
94+
const db = await getDb();
95+
const result = await db.put('Outcomes.NotificationClicked', {
96+
appId: APP_ID,
97+
notificationId: '1',
98+
timestamp: 1,
99+
});
100+
expect(result).toEqual('1');
101+
102+
const result2 = await db.put('Outcomes.NotificationReceived', {
103+
appId: APP_ID,
104+
notificationId: '1',
105+
timestamp: 1,
106+
});
107+
expect(result2).toEqual('1');
108+
});
109+
110+
// Tests NotificationClicked records migrate over from a v15 SDK version
111+
test('migrates notificationId type records into Outcomes.NotificationClicked', async () => {
112+
const db = await getDb(4);
113+
114+
await db.put('NotificationClicked', { notificationId: '1' });
115+
await db.put('NotificationClicked', { notificationId: '2' });
116+
await closeDb();
117+
118+
const db2 = await getDb(5);
119+
const result = await db2.getAll('Outcomes.NotificationClicked');
120+
expect(result).toEqual([
121+
{ appId: undefined, notificationId: '1', timestamp: undefined },
122+
{ appId: undefined, notificationId: '2', timestamp: undefined },
123+
]);
124+
125+
// old table should be removed
126+
expect(db2.objectStoreNames).not.toContain('NotificationClicked');
127+
});
128+
129+
// Tests NotificationReceived records migrate over from a v15 SDK version
130+
test('migrates notificationId type records into Outcomes.NotificationReceived', async () => {
131+
const db = await getDb(4);
132+
await db.put('NotificationReceived', {
133+
appId: APP_ID,
134+
notificationId: '1',
135+
timestamp: 1,
136+
});
137+
await db.put('NotificationReceived', {
138+
appId: APP_ID,
139+
notificationId: '2',
140+
timestamp: 1,
141+
});
142+
await closeDb();
143+
144+
const db2 = await getDb(5);
145+
const result = await db2.getAll('Outcomes.NotificationReceived');
146+
expect(result).toEqual([
147+
{ appId: APP_ID, notificationId: '1', timestamp: 1 },
148+
{ appId: APP_ID, notificationId: '2', timestamp: 1 },
149+
]);
150+
151+
// old table should be removed
152+
expect(db2.objectStoreNames).not.toContain('NotificationReceived');
153+
});
154+
155+
// Tests records coming from a broken SDK (160000.beta4 to 160000) and upgrading to fixed v5 db
156+
test('migrates notification.id type records into Outcomes.NotificationClicked', async () => {
157+
// 1. Put the db's schema into the broken v4 state that SDK v16000000 had
158+
const openDbRequest = indexedDB.open(DATABASE_NAME, 4);
159+
const dbOpenPromise = new Promise((resolve) => {
160+
openDbRequest.onsuccess = resolve;
161+
});
162+
const dbUpgradePromise = new Promise<void>((resolve) => {
163+
openDbRequest.onupgradeneeded = () => {
164+
const db = openDbRequest.result;
165+
db.createObjectStore('NotificationClicked', {
166+
keyPath: 'notification.id',
167+
});
168+
db.createObjectStore('NotificationReceived', {
169+
keyPath: 'notificationId',
170+
});
171+
resolve();
172+
};
173+
});
174+
await Promise.all([dbOpenPromise, dbUpgradePromise]);
175+
176+
// 2. Put a record into the DB with the old schema
177+
openDbRequest.result
178+
.transaction(['NotificationClicked'], 'readwrite')
179+
.objectStore('NotificationClicked')
180+
.put({ notification: { id: '1' } });
181+
openDbRequest.result.close();
182+
183+
// 3. Open the DB with the OneSignal IndexedDb class
184+
const db2 = await getDb(5);
185+
const result = await db2.getAll('Outcomes.NotificationClicked');
186+
// 4. Expect the that data is brought over to the new table.
187+
expect(result).toEqual([
188+
{ appId: undefined, notificationId: '1', timestamp: undefined },
189+
]);
190+
});
191+
});
192+
193+
describe('v6', () => {
194+
const populateLegacySubscriptions = async (
195+
db: IDBPDatabase<IndexedDBSchema>,
196+
) => {
197+
await db.put(LegacyModelName.EmailSubscriptions, {
198+
modelId: '1',
199+
modelName: LegacyModelName.EmailSubscriptions,
200+
onesignalId: DUMMY_ONESIGNAL_ID,
201+
type: SubscriptionType.Email,
202+
token: 'email-token',
203+
});
204+
await db.put(LegacyModelName.PushSubscriptions, {
205+
modelId: '2',
206+
modelName: LegacyModelName.PushSubscriptions,
207+
onesignalId: DUMMY_ONESIGNAL_ID,
208+
type: SubscriptionType.ChromePush,
209+
token: 'push-token',
210+
});
211+
await db.put(LegacyModelName.SmsSubscriptions, {
212+
modelId: '3',
213+
modelName: LegacyModelName.SmsSubscriptions,
214+
onesignalId: DUMMY_ONESIGNAL_ID,
215+
type: SubscriptionType.SMS,
216+
token: 'sms-token',
217+
});
218+
};
219+
220+
const migratedSubscriptions = {
221+
email: {
222+
modelId: '1',
223+
modelName: ModelName.Subscriptions,
224+
externalId: undefined,
225+
onesignalId: DUMMY_ONESIGNAL_ID,
226+
type: SubscriptionType.Email,
227+
token: 'email-token',
228+
},
229+
push: {
230+
modelId: '2',
231+
modelName: ModelName.Subscriptions,
232+
externalId: undefined,
233+
onesignalId: DUMMY_ONESIGNAL_ID,
234+
type: SubscriptionType.ChromePush,
235+
token: 'push-token',
236+
},
237+
sms: {
238+
modelId: '3',
239+
modelName: ModelName.Subscriptions,
240+
externalId: undefined,
241+
onesignalId: DUMMY_ONESIGNAL_ID,
242+
type: SubscriptionType.SMS,
243+
token: 'sms-token',
244+
},
245+
};
246+
247+
test('can write to new subscriptions table', async () => {
248+
const db = await getDb();
249+
const result = await db.put('subscriptions', {
250+
modelId: '1',
251+
modelName: 'subscriptions',
252+
onesignalId: '1',
253+
type: 'email',
254+
token: 'token',
255+
});
256+
expect(result).toEqual('1');
257+
});
258+
259+
test('migrates v5 email, push, sms subscriptions records to v6 subscriptions record', async () => {
260+
const db = await getDb(5);
261+
await populateLegacySubscriptions(db);
262+
await closeDb();
263+
264+
const db2 = await getDb(6);
265+
const result = await db2.getAll(ModelName.Subscriptions);
266+
expect(result).toEqual([
267+
migratedSubscriptions.email,
268+
migratedSubscriptions.push,
269+
migratedSubscriptions.sms,
270+
]);
271+
272+
// old tables should be removed
273+
const oldTableNames = [
274+
LegacyModelName.EmailSubscriptions,
275+
LegacyModelName.PushSubscriptions,
276+
LegacyModelName.SmsSubscriptions,
277+
];
278+
for (const tableName of oldTableNames) {
279+
expect(db2.objectStoreNames).not.toContain(tableName);
280+
}
281+
});
282+
283+
test('migrates v5 email, push, sms subscriptions records of logged in user to v6 subscriptions record with external id', async () => {
284+
const db = await getDb(5);
285+
await populateLegacySubscriptions(db);
286+
// user is logged in
287+
await db.put(ModelName.Identity, {
288+
modelId: '4',
289+
modelName: ModelName.Identity,
290+
onesignalId: DUMMY_ONESIGNAL_ID,
291+
externalId: DUMMY_EXTERNAL_ID,
292+
});
293+
await closeDb();
294+
295+
const db2 = await getDb(6);
296+
const result = await db2.getAll(ModelName.Subscriptions);
297+
expect(result).toEqual([
298+
{
299+
...migratedSubscriptions.email,
300+
externalId: DUMMY_EXTERNAL_ID,
301+
},
302+
{
303+
...migratedSubscriptions.push,
304+
externalId: DUMMY_EXTERNAL_ID,
305+
},
306+
{
307+
...migratedSubscriptions.sms,
308+
externalId: DUMMY_EXTERNAL_ID,
309+
},
310+
]);
311+
});
312+
});
313+
});

src/shared/database/client.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ import {
1616
let dbInstance: Awaited<ReturnType<typeof openDB<IndexedDBSchema>>> | null =
1717
null;
1818

19-
export const getDb = async () => {
19+
export const getDb = async (version = VERSION) => {
2020
if (dbInstance) return dbInstance;
21-
dbInstance = await openDB<IndexedDBSchema>(DATABASE_NAME, VERSION, {
21+
dbInstance = await openDB<IndexedDBSchema>(DATABASE_NAME, version, {
2222
async upgrade(_db, oldVersion, newVersion, transaction) {
23-
const newDbVersion = newVersion || VERSION;
23+
const newDbVersion = newVersion || version;
2424
if (newDbVersion >= 1 && oldVersion < 1) {
2525
_db.createObjectStore('Ids', { keyPath: 'type' });
2626
_db.createObjectStore('NotificationOpened', { keyPath: 'url' });
@@ -83,7 +83,7 @@ type StoreName = StoreNames<IndexedDBSchema>;
8383
export const db = {
8484
async get<K extends StoreName>(
8585
storeName: K,
86-
key: string,
86+
key: IndexedDBSchema[K]['key'],
8787
): Promise<IndexedDBSchema[K]['value'] | undefined> {
8888
const _db = await getDb();
8989
return _db.get(storeName, key);
@@ -101,7 +101,10 @@ export const db = {
101101
const _db = await getDb();
102102
return _db.put(storeName, value);
103103
},
104-
async delete<K extends StoreName>(storeName: K, key: string) {
104+
async delete<K extends StoreName>(
105+
storeName: K,
106+
key: IndexedDBSchema[K]['key'],
107+
) {
105108
const _db = await getDb();
106109
return _db.delete(storeName, key);
107110
},
@@ -136,10 +139,14 @@ export const cleanupCurrentSession = async () => {
136139
await db.delete('Sessions', ONESIGNAL_SESSION_KEY);
137140
};
138141

139-
// check if used in non tests
140142
export const clearAll = async () => {
141143
const objectStoreNames = db.objectStoreNames;
142144
for (const storeName of objectStoreNames) {
143145
await db.clear(storeName);
144146
}
145147
};
148+
149+
export const closeDb = async () => {
150+
await dbInstance?.close();
151+
dbInstance = null;
152+
};

0 commit comments

Comments
 (0)