Skip to content

Commit

Permalink
fix(arns): fix undername resolution, add e2e tests
Browse files Browse the repository at this point in the history
The arns name cache only contains base names. When an undername is requested, it fails to find the full undername in the cache and results in a 404. This modifies the behavior to parse out the base name from an undername, and check the arns name cache for that base name first. Then it checks the resolution cache for the full name data of the undername. Additional e2e tests have been added to ensure this behavior does not regress in future modifications to arns handling.
  • Loading branch information
dtfiedler authored and djwhitt committed Jan 8, 2025
1 parent f8f6155 commit 82f5eac
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 83 deletions.
23 changes: 18 additions & 5 deletions src/resolution/composite-arns-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class CompositeArNSResolver implements NameResolver {
// TODO: other overrides like fallback txId if not found in resolution
}
| undefined;
private arnsNamesCache: ArNSNamesCache;
private arnsBaseNamesCache: ArNSNamesCache;

constructor({
log,
Expand All @@ -50,20 +50,33 @@ export class CompositeArNSResolver implements NameResolver {
this.resolvers = resolvers;
this.cache = cache;
this.overrides = overrides;
this.arnsNamesCache = new ArNSNamesCache({ log });
this.arnsBaseNamesCache = new ArNSNamesCache({ log });
}

async resolve(name: string): Promise<NameResolution> {
this.log.info('Resolving name...', { name, overrides: this.overrides });
const arnsNamesCache = await this.arnsNamesCache.getNames();
const arnsBaseNamesCache = await this.arnsBaseNamesCache.getNames();
let resolution: NameResolution | undefined;

if (arnsNamesCache.size === 0) {
if (arnsBaseNamesCache.size === 0) {
this.log.debug('Cached ArNS names list is empty');
}

if (arnsNamesCache.has(name)) {
// parse out base arns name, if undername
const baseName = name.split('_').pop();
if (baseName === undefined) {
return {
name,
resolvedId: undefined,
resolvedAt: undefined,
ttl: undefined,
processId: undefined,
};
}

if (arnsBaseNamesCache.has(baseName)) {
try {
// check if our resolution cache contains the FULL name
const cachedResolutionBuffer = await this.cache.get(name);
if (cachedResolutionBuffer) {
const cachedResolution: NameResolution = JSON.parse(
Expand Down
2 changes: 1 addition & 1 deletion src/resolution/on-demand-arns-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class OnDemandArNSResolver implements NameResolver {
async resolve(name: string): Promise<NameResolution> {
this.log.info('Resolving name...', { name });
try {
// get the base name which is the last of th array split by _
// get the base name which is the last of the array split by _
const baseName = name.split('_').pop();
if (baseName === undefined) {
throw new Error('Invalid name');
Expand Down
219 changes: 142 additions & 77 deletions test/end-to-end/arns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,98 +50,163 @@ after(async function () {
});

describe('ArNS', function () {
it('Verifying that "__unknown__.ar-io.localhost" returns 404', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: '__unknown__.ar-io.localhost' },
validateStatus: () => true,
describe('Subdomain resolution', function () {
describe('Base names', function () {
it('Verifying "__unknown__.ar-io.localhost" returns 404', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: '__unknown__.ar-io.localhost' },
validateStatus: () => true,
});

assert.strictEqual(res.status, 404);
});

it('Verifying "ardrive.ar-io.localhost" returns 200', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'ardrive.ar-io.localhost' },
});

assert.strictEqual(res.status, 200);
});

it('Verifying "ardrive.ar-io.localhost" X-ArNS-Resolved-ID header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'ardrive.ar-io.localhost' },
});

assert.strictEqual(typeof res.headers['x-arns-resolved-id'], 'string');
});

it('Verifying "ardrive.ar-io.localhost" X-ArNS-TTL-Seconds header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'ardrive.ar-io.localhost' },
});

assert.strictEqual(typeof res.headers['x-arns-ttl-seconds'], 'string');
});

it('Verifying "ardrive.ar-io.localhost" X-ArNS-Process-ID header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'ardrive.ar-io.localhost' },
});

assert.strictEqual(typeof res.headers['x-arns-process-id'], 'string');
});

it('Verifying "ardrive.ar-io.localhost/{txid}" is redirected', async function () {
const txId = 'TB2wJyKrPnkAW79DAwlJYwpgdHKpijEJWQfcwX715Co';
const expectedSandbox =
'jqo3ajzcvm7hsac3x5bqgckjmmfga5dsvgfdcckza7omc7xv4qva';
const expectedRedirect = `https://${expectedSandbox}.ar-io.localhost/${txId}?`;
const res = await axios.get(`http://localhost:4000/${txId}`, {
headers: { Host: 'ardrive.ar-io.localhost' },
maxRedirects: 0, // Prevent axios from following redirects
validateStatus: function (status) {
return status === 302; // Accept only 302 status
},
});

// Assert the status code is 302
assert.strictEqual(res.status, 302);

// Assert the Location header matches the expected URL
assert.strictEqual(res.headers['location'], expectedRedirect);
});
});

assert.strictEqual(res.status, 404);
});
describe('Undernames', function () {
it('Verifying "dapp_ardrive.ar-io.localhost" returns 200', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'dapp_ardrive.ar-io.localhost' },
});

it('Verifying that "ardrive.ar-io.localhost" returns 200', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'ardrive.ar-io.localhost' },
});
assert.strictEqual(res.status, 200);
});

assert.strictEqual(res.status, 200);
});
it('Verifying "dapp_ardrive.ar-io.localhost" X-ArNS-Resolved-ID header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'dapp_ardrive.ar-io.localhost' },
});

it('Verifying "ardrive.ar-io.localhost" X-ArNS-Resolved-ID header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'ardrive.ar-io.localhost' },
});
assert.strictEqual(typeof res.headers['x-arns-resolved-id'], 'string');
});

assert.strictEqual(typeof res.headers['x-arns-resolved-id'], 'string');
});
it('Verifying "dapp_ardrive.ar-io.localhost" X-ArNS-TTL-Seconds header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'dapp_ardrive.ar-io.localhost' },
});

it('Verifying "ardrive.ar-io.localhost" X-ArNS-TTL-Seconds header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'ardrive.ar-io.localhost' },
});
assert.strictEqual(typeof res.headers['x-arns-ttl-seconds'], 'string');
});

assert.strictEqual(typeof res.headers['x-arns-ttl-seconds'], 'string');
});
it('Verifying "dapp_ardrive.ar-io.localhost" X-ArNS-Process-ID header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'dapp_ardrive.ar-io.localhost' },
});

it('Verifying "ardrive.ar-io.localhost" X-ArNS-Process-ID header', async function () {
const res = await axios.get('http://localhost:4000', {
headers: { Host: 'ardrive.ar-io.localhost' },
assert.strictEqual(typeof res.headers['x-arns-process-id'], 'string');
});
});

assert.strictEqual(typeof res.headers['x-arns-process-id'], 'string');
});

it('Verifying that "ardrive.ar-io.localhost/{txid}" is redirected', async function () {
const txId = 'TB2wJyKrPnkAW79DAwlJYwpgdHKpijEJWQfcwX715Co';
const expectedSandbox =
'jqo3ajzcvm7hsac3x5bqgckjmmfga5dsvgfdcckza7omc7xv4qva';
const expectedRedirect = `https://${expectedSandbox}.ar-io.localhost/${txId}?`;
const res = await axios.get(`http://localhost:4000/${txId}`, {
headers: { Host: 'ardrive.ar-io.localhost' },
maxRedirects: 0, // Prevent axios from following redirects
validateStatus: function (status) {
return status === 302; // Accept only 302 status
},
describe('Resolver endpoint resolution', function () {
// verify the resolution of an undername
it('Verifying /ar-io/resolver/ardrive returns 200 and resolution data', async function () {
const res = await axios.get(
'http://localhost:4000/ar-io/resolver/ardrive',
);

assert.strictEqual(res.status, 200);
assert.strictEqual(
typeof res.data.txId === 'string' && res.data.txId.length === 43,
true,
);
assert.strictEqual(typeof res.data.ttlSeconds, 'number');
assert.strictEqual(typeof res.data.processId, 'string');
});

// Assert that the status code is 302
assert.strictEqual(res.status, 302);
// verify the headers are set correctly on the response
it('Verifying /ar-io/resolver/ardrive returns 200 and sets the correct headers', async function () {
const res = await axios.get(
'http://localhost:4000/ar-io/resolver/ardrive',
);

assert.strictEqual(
typeof res.headers['x-arns-resolved-id'] === 'string' &&
res.headers['x-arns-resolved-id'].length === 43,
true,
);
assert.strictEqual(typeof res.headers['x-arns-ttl-seconds'], 'string');
assert.strictEqual(typeof res.headers['x-arns-process-id'], 'string');
});

// Assert that the Location header matches the expected URL
assert.strictEqual(res.headers['location'], expectedRedirect);
});
it('Verifying /ar-io/resolver/dapp_ardrive returns 200 and resolution data for an undername', async function () {
const res = await axios.get(
'http://localhost:4000/ar-io/resolver/dapp_ardrive',
);

assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.data.txId, 'string');
assert.strictEqual(typeof res.data.ttlSeconds, 'number');
assert.strictEqual(typeof res.data.processId, 'string');
assert.strictEqual(
typeof res.headers['x-arns-resolved-id'] === 'string' &&
res.headers['x-arns-resolved-id'].length === 43,
true,
);
assert.strictEqual(typeof res.headers['x-arns-ttl-seconds'], 'string');
assert.strictEqual(typeof res.headers['x-arns-process-id'], 'string');
});

it('Verifying /ar-io/resolver/<non-existent-name> returns 404 for nonexistent name', async function () {
const res = await axios.get(
'http://localhost:4000/ar-io/resolver/nonexistent',
{
validateStatus: (status) => status === 404, // only accept 404 status
},
);

// FIXME: these can't depend on names that change frequently

//// verify the /ar-io/resolver/:name endpoint
//it('Verifying that /ar-io/resolver/:name returns 200 and resolution data', async function() {
// const txId = 'rhYmL-2y7NC1SX9TauCNzNK9QmZ3h6Vd4pfCC8fgGcQ';
// const res = await axios.get('http://localhost:4000/ar-io/resolver/ardrive');

// assert.strictEqual(res.status, 200);
// assert.strictEqual(res.data.txId, txId);
// assert.strictEqual(typeof res.data.ttlSeconds, 'number');
// assert.strictEqual(typeof res.data.processId, 'string');
//});

//// verify the headers are set correctly on the response
//it('Verifying that /ar-io/resolver/:name returns 200 and sets the correct headers', async function() {
// const txId = 'rhYmL-2y7NC1SX9TauCNzNK9QmZ3h6Vd4pfCC8fgGcQ';
// const res = await axios.get('http://localhost:4000/ar-io/resolver/ardrive');

// assert.strictEqual(res.headers['x-arns-resolved-id'], txId);
// assert.strictEqual(typeof res.headers['x-arns-ttl-seconds'], 'string');
// assert.strictEqual(typeof res.headers['x-arns-process-id'], 'string');
//});

it('Verifying that /ar-io/resolver/:name returns 404 for nonexistent name', async function () {
const res = await axios.get(
'http://localhost:4000/ar-io/resolver/nonexistent',
{
validateStatus: (status) => status === 404, // only accept 404 status
},
);

assert.strictEqual(res.status, 404);
assert.strictEqual(res.status, 404);
});
});
});

0 comments on commit 82f5eac

Please sign in to comment.