Files
puter/tests/puterJsApiTests/kv.test.ts
T
Daniel Salazar e75ccb0a41 feat: root level kv accesses, and installed app listing + server health check fix (#2719)
* feat: root level kv accesses, and installed app listing

* fix: revert server health check
2026-03-24 19:18:42 -07:00

286 lines
12 KiB
TypeScript

// kv.test.ts - Tests for Puter KV module (set, get, del, incr, decr, list, flush)
import { describe, expect, it } from 'vitest';
import { puter } from './testUtils.js';
describe('Puter KV Module', () => {
const TEST_KEY = 'test-key';
it('should set a key success', async () => {
await expect(puter.kv.set(TEST_KEY, 0)).resolves.toBe(true);
});
it('should get a key success', async () => {
const getRes = await puter.kv.get(TEST_KEY);
expect(getRes).toBe(0);
});
it('should get empty key', async () => {
const emptyRes = await puter.kv.get(`fake${ TEST_KEY}`);
expect(emptyRes).toBeNull();
});
it('should increment a key success', async () => {
await puter.kv.set(TEST_KEY, 0);
const incrRes = await puter.kv.incr(TEST_KEY, { '': 5 });
expect(incrRes).toBe(5);
const finalGet = await puter.kv.get(TEST_KEY);
expect(finalGet).toBe(5);
});
it('should decrement a key success', async () => {
await puter.kv.set(TEST_KEY, 0);
const decrRes = await puter.kv.decr(TEST_KEY, { '': 3 });
expect(decrRes).toBe(-3);
const finalGet = await puter.kv.get(TEST_KEY);
expect(finalGet).toBe(-3);
});
it('should increment a key with second argument', async () => {
await puter.kv.set(TEST_KEY, 0);
const incrRes = await puter.kv.incr(TEST_KEY);
expect(incrRes).toBe(1);
const finalGet = await puter.kv.get(TEST_KEY);
expect(finalGet).toBe(1);
});
it('should decrement a key with second argument', async () => {
await puter.kv.set(TEST_KEY, 0);
const incrRes = await puter.kv.decr(TEST_KEY);
expect(incrRes).toBe(-1);
const finalGet = await puter.kv.get(TEST_KEY);
expect(finalGet).toBe(-1);
});
it('should increment a key with second argument', async () => {
await puter.kv.set(TEST_KEY, 0);
const incrRes = await puter.kv.incr(TEST_KEY, 2);
expect(incrRes).toBe(2);
const finalGet = await puter.kv.get(TEST_KEY);
expect(finalGet).toBe(2);
});
it('should decrement a key with second argument', async () => {
await puter.kv.set(TEST_KEY, 0);
const incrRes = await puter.kv.decr(TEST_KEY, 3);
expect(incrRes).toBe(-3);
const finalGet = await puter.kv.get(TEST_KEY);
expect(finalGet).toBe(-3);
});
it('should increment a key with nested path', async () => {
await puter.kv.set(TEST_KEY, { a: { b: 0 } });
const incrRes = await puter.kv.incr(TEST_KEY, { 'a.b': 1 });
expect(incrRes).toEqual({ a: { b: 1 } });
const finalGet = await puter.kv.get(TEST_KEY);
expect(finalGet).toEqual({ a: { b: 1 } });
});
it('should decrement a key with nested path', async () => {
await puter.kv.set(TEST_KEY, { a: { b: 0 } });
const incrRes = await puter.kv.decr(TEST_KEY, { 'a.b': 1 });
expect(incrRes).toEqual({ a: { b: -1 } });
const finalGet = await puter.kv.get(TEST_KEY);
expect(finalGet).toEqual({ a: { b: -1 } });
});
it('should increment a nonexistent key with nested path', async () => {
const incrRes = await puter.kv.incr(TEST_KEY + 1, { 'a.b': 1 });
expect(incrRes).toEqual({ a: { b: 1 } });
const finalGet = await puter.kv.get(TEST_KEY + 1);
expect(finalGet).toEqual({ a: { b: 1 } });
});
it('should decrement a nonexistent key with nested path', async () => {
const incrRes = await puter.kv.decr(TEST_KEY + 2, { 'a.b': 1 });
expect(incrRes).toEqual({ a: { b: -1 } });
const finalGet = await puter.kv.get(TEST_KEY + 2);
expect(finalGet).toEqual({ a: { b: -1 } });
});
it('should update a key with nested path', async () => {
const updateKey = `${TEST_KEY}-update`;
await puter.kv.set(updateKey, { profile: { name: 'old' } });
const updateRes = await puter.kv.update(updateKey, { 'profile.name': 'new' });
expect(updateRes).toEqual({ profile: { name: 'new' } });
const finalGet = await puter.kv.get(updateKey);
expect(finalGet).toEqual({ profile: { name: 'new' } });
});
it('should update a key with indexed path', async () => {
const updateKey = `${TEST_KEY}-update-index`;
await puter.kv.set(updateKey, { a: { b: [1, 2] } });
const updateRes = await puter.kv.update(updateKey, { 'a.b[1]': 5 });
expect(updateRes).toEqual({ a: { b: [1, 5] } });
const finalGet = await puter.kv.get(updateKey);
expect(finalGet).toEqual({ a: { b: [1, 5] } });
});
it('should add values into list paths', async () => {
const addKey = `${TEST_KEY}-add`;
const addRes = await puter.kv.add(addKey, { 'a.b': 1 });
expect(addRes).toEqual({ a: { b: [1] } });
const secondAdd = await puter.kv.add(addKey, { 'a.b': 2, 'a.c': ['x'] });
expect(secondAdd).toEqual({ a: { b: [1, 2], c: ['x'] } });
const finalGet = await puter.kv.get(addKey);
expect(finalGet).toEqual({ a: { b: [1, 2], c: ['x'] } });
});
it('should add values into indexed list paths', async () => {
const addKey = `${TEST_KEY}-add-index`;
await puter.kv.set(addKey, { a: { b: [[1], [2]] } });
const addRes = await puter.kv.add(addKey, { 'a.b[1]': 3 });
expect(addRes).toEqual({ a: { b: [[1], [2, 3]] } });
const finalGet = await puter.kv.get(addKey);
expect(finalGet).toEqual({ a: { b: [[1], [2, 3]] } });
});
it('should remove nested values including indexed list paths', async () => {
const removeKey = `${TEST_KEY}-remove`;
await puter.kv.set(removeKey, { a: { b: [1, 2, 3], c: { d: 4 }, e: 'keep' } });
const removeRes = await puter.kv.remove(removeKey, 'a.b[1]', 'a.c');
expect(removeRes).toEqual({ a: { b: [1, 3], e: 'keep' } });
const finalGet = await puter.kv.get(removeKey);
expect(finalGet).toEqual({ a: { b: [1, 3], e: 'keep' } });
});
it('should list keys', async () => {
const listRes = await puter.kv.list();
expect(Array.isArray(listRes)).toBe(true);
expect(listRes.length).toBeGreaterThan(0);
expect((listRes as string[]).includes(TEST_KEY)).toBe(true);
});
it('should support prefix pattern semantics', async () => {
const basePrefix = `${TEST_KEY}-pattern-abc`;
const abcKeys = [
`${basePrefix}`,
`${basePrefix}123`,
`${basePrefix}123xyz`,
];
await Promise.all(abcKeys.map((key) => puter.kv.set(key, key)));
await puter.kv.set(`${TEST_KEY}-pattern-ab`, 'nope');
const listNoWildcard = await puter.kv.list(basePrefix);
const listWildcard = await puter.kv.list(`${basePrefix}*`);
expect(Array.isArray(listNoWildcard)).toBe(true);
expect(Array.isArray(listWildcard)).toBe(true);
expect(listNoWildcard).toEqual(expect.arrayContaining(abcKeys));
expect(listWildcard).toEqual(expect.arrayContaining(abcKeys));
expect((listNoWildcard as string[]).every((key) => key.startsWith(basePrefix))).toBe(true);
expect((listWildcard as string[]).every((key) => key.startsWith(basePrefix))).toBe(true);
const literalStarPrefix = `${TEST_KEY}-pattern-key*`;
const literalStarKeys = [
`${literalStarPrefix}one`,
`${literalStarPrefix}two`,
];
await Promise.all(literalStarKeys.map((key) => puter.kv.set(key, key)));
const literalStarList = await puter.kv.list(`${literalStarPrefix}*`);
expect(Array.isArray(literalStarList)).toBe(true);
expect(literalStarList).toEqual(expect.arrayContaining(literalStarKeys));
expect((literalStarList as string[]).every((key) => key.startsWith(literalStarPrefix))).toBe(true);
const middleStarPrefix = `${TEST_KEY}-pattern-k*y`;
const middleStarKeys = [
`${middleStarPrefix}`,
`${middleStarPrefix}-more`,
];
await Promise.all(middleStarKeys.map((key) => puter.kv.set(key, key)));
const middleStarList = await puter.kv.list(`${middleStarPrefix}*`);
expect(Array.isArray(middleStarList)).toBe(true);
expect(middleStarList).toEqual(expect.arrayContaining(middleStarKeys));
expect((middleStarList as string[]).every((key) => key.startsWith(middleStarPrefix))).toBe(true);
const allList = await puter.kv.list('*');
expect(Array.isArray(allList)).toBe(true);
expect(allList).toEqual(expect.arrayContaining([...abcKeys, ...literalStarKeys, ...middleStarKeys]));
});
it('should list keys with pagination', async () => {
const pageKeys = [
`${TEST_KEY}-page-1`,
`${TEST_KEY}-page-2`,
`${TEST_KEY}-page-3`,
];
await puter.kv.set(pageKeys[0], 'one');
await puter.kv.set(pageKeys[1], 'two');
await puter.kv.set(pageKeys[2], 'three');
const firstPage = await puter.kv.list({ limit: 1 });
expect(Array.isArray(firstPage)).toBe(false);
const firstPageObj = firstPage as { items: string[]; cursor?: string };
expect(Array.isArray(firstPageObj.items)).toBe(true);
expect(firstPageObj.items.length).toBeLessThanOrEqual(1);
expect(firstPageObj.cursor).toBeTypeOf('string');
const secondPage = await puter.kv.list({ limit: 1, cursor: firstPageObj.cursor });
const secondPageObj = secondPage as { items: string[]; cursor?: string };
expect(Array.isArray(secondPageObj.items)).toBe(true);
expect(secondPageObj.items.length).toBeLessThanOrEqual(1);
});
it('should isolate namespaces when using optConfig.appUuid', async () => {
const suffix = Date.now().toString(36);
const key = `${TEST_KEY}-opt-override-${suffix}`;
const overrideA = { appUuid: `opt-a-${suffix}` };
const overrideB = { appUuid: `opt-b-${suffix}` };
await puter.kv.set(key, 'default-value');
await puter.kv.set(key, 'override-a-value', overrideA);
await puter.kv.set(key, 'override-b-value', overrideB);
expect(await puter.kv.get(key)).toBe('default-value');
expect(await puter.kv.get(key, overrideA)).toBe('override-a-value');
expect(await puter.kv.get(key, overrideB)).toBe('override-b-value');
const listA = await puter.kv.list(`${key}*`, overrideA);
expect(Array.isArray(listA)).toBe(true);
expect((listA as string[])).toContain(key);
await puter.kv.del(key, overrideA);
expect(await puter.kv.get(key, overrideA)).toBeNull();
expect(await puter.kv.get(key)).toBe('default-value');
});
it('should support optConfig shorthand and scoped flush', async () => {
const suffix = Date.now().toString(36);
const overrideA = { appUuid: `opt-shorthand-a-${suffix}` };
const overrideB = { appUuid: `opt-shorthand-b-${suffix}` };
const counterKey = `${TEST_KEY}-opt-counter-${suffix}`;
const updateKey = `${TEST_KEY}-opt-update-${suffix}`;
const flushKeyA = `${TEST_KEY}-opt-flush-a-${suffix}`;
const flushKeyB = `${TEST_KEY}-opt-flush-b-${suffix}`;
const incrRes = await puter.kv.incr(counterKey, overrideA);
expect(incrRes).toBe(1);
expect(await puter.kv.get(counterKey)).toBeNull();
expect(await puter.kv.get(counterKey, overrideA)).toBe(1);
const updateRes = await puter.kv.update(updateKey, { 'profile.name': 'Ada' }, overrideA);
expect(updateRes).toEqual({ profile: { name: 'Ada' } });
expect(await puter.kv.get(updateKey)).toBeNull();
expect(await puter.kv.get(updateKey, overrideA)).toEqual({ profile: { name: 'Ada' } });
await puter.kv.set(flushKeyA, 'A', overrideA);
await puter.kv.set(flushKeyB, 'B', overrideB);
await puter.kv.flush(overrideA);
expect(await puter.kv.get(flushKeyA, overrideA)).toBeNull();
expect(await puter.kv.get(flushKeyB, overrideB)).toBe('B');
});
// delete ops should go last
it('should flush all keys', async () => {
const flushRes = await puter.kv.flush();
expect(flushRes).toBe(true);
const postFlushList = await puter.kv.list();
expect(Array.isArray(postFlushList)).toBe(true);
expect(postFlushList.length).toBe(0);
});
it('should delete a key success', async () => {
const setRes = await puter.kv.set(TEST_KEY, 'to-be-deleted');
expect(setRes).toBe(true);
const delRes = await puter.kv.del(TEST_KEY);
expect(delRes).toBe(true);
const getRes = await puter.kv.get(TEST_KEY);
expect(getRes).toBeNull();
});
});