Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions hypercerts-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Hypercerts API installer foundation

This package provides the conflict-safe HappyView admin installer, pinned Lexicon dependencies, deterministic location fixtures, and guarded seeding tools used by the location API. It does not yet include an installable manifest or Lua handlers; those are added in the dependent location API PR. Do not run `tooling/installer.js` against a target from this branch.

Fixture CIDs are computed from stored record JSON with `@atcute/cbor` and `@atcute/cid`. If you previously seeded these fixtures, rerun the guarded seed command on the disposable test database before comparing CIDs: the blob-backed fixture's CID changed because the previous `jsonToLex` conversion added a field absent from stored JSON.

From this directory, install dependencies with `pnpm install --frozen-lockfile`, then run offline checks with `pnpm test:unit`. The installer reads existing assets first, refuses unexpected differences, and writes missing assets in dependency order. Installing assets requires the complete location package and an explicitly approved HappyView target. Seeding bypasses ingestion and is only for a separately approved, disposable loopback PostgreSQL test database; never use these fixtures on persistent data. See the dependent PR for the full install and contract-test procedure.
17 changes: 17 additions & 0 deletions hypercerts-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@happyview/hypercerts-api-test-kit",
"private": true,
"type": "module",
"dependencies": {
"@atcute/cbor": "2.3.2",
"@atcute/cid": "2.4.1",
"@atproto/lexicon": "0.7.4",
"@atproto/syntax": "0.6.4",
"@hypercerts-org/lexicon": "1.4.0"
},
"scripts": {
"test:unit": "node --test tooling/*.test.js tests/fixtures/*.test.js tests/contracts/helpers.test.js",
"seed:test": "node tooling/seed.js",
"seed:bad-dates": "node tooling/seed.js --bad-dates"
}
}
231 changes: 231 additions & 0 deletions hypercerts-api/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions hypercerts-api/tests/contracts/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function requireContractTarget(env = process.env) {
if (!env.HAPPYVIEW_BASE_URL) throw new Error('HAPPYVIEW_BASE_URL must point to the supplied running local HappyView instance');
const url = new URL(env.HAPPYVIEW_BASE_URL);
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('HAPPYVIEW_BASE_URL must use HTTP or HTTPS');
if (!['localhost', '127.0.0.1', '::1'].includes(url.hostname.replace(/^\[|\]$/g, ''))) throw new Error('HTTP contracts are restricted to a local HappyView URL');
return url;
}

export function contractUrl(baseUrl, nsid, params = {}) {
const url = new URL(`/xrpc/${nsid}`, baseUrl);
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) for (const item of value) url.searchParams.append(key, item);
else if (value !== undefined && value !== null) url.searchParams.append(key, String(value));
}
// HappyView's current query decoder mishandles '+' as a space; encode spaces as %20.
url.search = url.search.replaceAll('+', '%20');
return url.toString();
}
14 changes: 14 additions & 0 deletions hypercerts-api/tests/contracts/helpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { contractUrl, requireContractTarget } from './helpers.js';

test('repeated arrays are encoded as unbracketed query keys and empty optional arrays are omitted', () => {
assert.equal(contractUrl('http://127.0.0.1:8080', 'app.certified.location.listLocations', { authors: ['did:plc:a', 'did:plc:b'], uris: [], search: 'river bank' }),
'http://127.0.0.1:8080/xrpc/app.certified.location.listLocations?authors=did%3Aplc%3Aa&authors=did%3Aplc%3Ab&search=river%20bank');
});

test('contract target must be an explicit supplied HTTP(S) URL', () => {
assert.throws(() => requireContractTarget({}), /HAPPYVIEW_BASE_URL/);
assert.equal(requireContractTarget({ HAPPYVIEW_BASE_URL: 'http://127.0.0.1:8000' }).origin, 'http://127.0.0.1:8000');
assert.throws(() => requireContractTarget({ HAPPYVIEW_BASE_URL: 'https://happyview.example.com' }), /restricted to a local HappyView URL/);
});
46 changes: 46 additions & 0 deletions hypercerts-api/tests/fixtures/bad-location-dates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Deliberately invalid record dates: use only with an explicitly disposable PostgreSQL test target.
// These bypass normal Lexicon validation and must never be added to records.js or routine seeding.
import { encode } from '@atcute/cbor';
import * as CID from '@atcute/cid';
import { locationRecords } from './records.js';

const base = locationRecords[0];
const did = 'did:plc:baddatefixturesexamplexx';
const collection = base.collection;
const prefix = `at://${did}/${collection}/`;

const cases = [
['badmissing', undefined, '2025-01-02T03:04:05.123456Z'],
['badnull', null, '2025-01-02T03:04:05.123456Z'],
['badnumber', 123, '2025-01-02T03:04:05.123456Z'],
['badarray', ['2025-01-01T00:00:00Z'], '2025-01-02T03:04:05.123456Z'],
['badtext', 'not-a-date', '2025-01-02T03:04:05.123456Z'],
['badnozone', '2025-01-01T00:00:00', '2025-01-02T03:04:05.123456Z'],
['badhour', '2025-01-01T24:00:00Z', '2025-01-02T03:04:05.123456Z'],
['badzone', '2025-01-01T00:00:00+00:60', '2025-01-02T03:04:05.123456Z'],
['badcalendar', '2025-02-29T00:00:00Z', '2025-01-02T03:04:05.123456Z'],
['badunindexed', 'invalid', null],
['goodoffset', '2025-01-02T08:34:05.123456+05:30', '2025-01-06T00:00:00Z'],
['goodfraction', '2025-01-02T03:04:05.123456Z', '2025-01-07T00:00:00Z'],
['goodnano', '2025-01-02T03:04:05.123456789Z', '2025-01-09T00:00:00Z'],
['goodearlier', '2025-01-01T00:00:00Z', '2025-01-08T00:00:00Z'],
];

export const badDateLocations = await Promise.all(cases.map(async ([rkey, createdAt, indexedAt]) => {
const record = { ...base.record, locationType: 'date-test', name: `Date test ${rkey}`, description: 'Synthetic timestamp fixture' };
if (createdAt === undefined) delete record.createdAt;
else record.createdAt = createdAt;
return {
uri: prefix + rkey, did, collection, rkey, record, indexedAt,
storedAt: '2025-01-02T03:04:05.123456Z',
cid: CID.toString(await CID.create(0x71, encode(record))),
};
}));

export function badDateSeedSql({ disposableTestTarget = false } = {}) {
if (disposableTestTarget !== true) throw new Error('Bad-date seeding requires explicit disposable-test target opt-in');
return badDateLocations.map((row) => ({
sql: 'INSERT INTO happyview_records (uri, did, collection, rkey, record, cid, indexed_at, created_at) VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8) ON CONFLICT (uri) DO UPDATE SET record = EXCLUDED.record, cid = EXCLUDED.cid, indexed_at = EXCLUDED.indexed_at, created_at = EXCLUDED.created_at',
params: [row.uri, row.did, row.collection, row.rkey, JSON.stringify(row.record), row.cid, row.indexedAt, row.storedAt],
}));
}
37 changes: 37 additions & 0 deletions hypercerts-api/tests/fixtures/bad-location-dates.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { badDateLocations, badDateSeedSql } from './bad-location-dates.js';
import { locationRecords, profileRecords, organizationRecords } from './records.js';

test('bad-date rows require explicit disposable-target opt-in and preserve malformed JSON values', () => {
assert.throws(() => badDateSeedSql(), /disposable-test target opt-in/);
const statements = badDateSeedSql({ disposableTestTarget: true });
assert.equal(statements.length, badDateLocations.length);
const missing = statements.find(({ params }) => params[3] === 'badmissing');
const absent = JSON.parse(missing.params[4]);
assert.equal(Object.hasOwn(absent, 'createdAt'), false);
const unindexed = statements.find(({ params }) => params[3] === 'badunindexed');
assert.equal(unindexed.params[6], null);
assert.equal(unindexed.params[7], '2025-01-02T03:04:05.123456Z');
});

test('bad-date fixtures have their own author and do not match normal fixture filters', () => {
const normal = [...locationRecords, ...profileRecords, ...organizationRecords];
const normalDids = new Set(normal.map(({ did }) => did));
const normalUris = new Set(normal.map(({ uri }) => uri));
assert.equal(new Set(badDateLocations.map(({ uri }) => uri)).size, badDateLocations.length);
for (const row of badDateLocations) {
assert.equal(row.did, 'did:plc:baddatefixturesexamplexx');
assert.equal(row.uri, `at://${row.did}/${row.collection}/${row.rkey}`);
assert.equal(normalDids.has(row.did), false);
assert.equal(normalUris.has(row.uri), false);
assert.equal(row.record.locationType, 'date-test');
assert.equal(`${row.record.name} ${row.record.description}`.toLowerCase().includes('community forest'), false);
assert.equal(`${row.record.name} ${row.record.description}`.includes(String.raw`100%_\path`), false);
}
const statements = badDateSeedSql({ disposableTestTarget: true });
for (let i = 0; i < statements.length; i++) {
assert.equal(statements[i].params[0], badDateLocations[i].uri);
assert.equal(statements[i].params[1], badDateLocations[i].did);
}
});
Loading
Loading