Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added e2e tests for API endpoints #62

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
8 changes: 5 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ services:
- NODE_ENV=testing
- SKIP_SIGNATURE_VERIFICATION=true
- ALLOW_PRIVATE_ADDRESS=true
- NODE_TLS_REJECT_UNAUTHORIZED=0
Copy link
Member Author

@mike182uk mike182uk Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allouis I needed to add this otherwise the webfinger lookup on the fake-external-activitypub would not work.

When Fedify does the lookup based on just being provided a handle (@[email protected]) it defaults to using https so i had to expose the --https-port on the wiremock containers. This uses a self signed certificate, so had to enable NODE_TLS_REJECT_UNAUTHORIZED so that node didn't complain about the certificate.

Not sure if i've not set something up correct somewhere thats meaning i need to do this?

command: node --import tsx --watch src/app.ts
depends_on:
mysql-testing:
Expand Down Expand Up @@ -112,28 +113,29 @@ services:
activitypub-testing:
condition: service_healthy


mysql-testing:
image: mysql:8.0.31
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_USER=ghost
- MYSQL_PASSWORD=password
- MYSQL_DATABASE=activitypub
ports:
- "3308:3306"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useful for debugging test db

healthcheck:
test: "mysql -ughost -ppassword activitypub -e 'select 1'"
interval: 1s
retries: 120

fake-ghost-activitypub:
image: wiremock/wiremock:3.9.2-1
entrypoint: ["/docker-entrypoint.sh", "--global-response-templating", "--disable-gzip", "--verbose", "--port=80"]
entrypoint: ["/docker-entrypoint.sh", "--global-response-templating", "--disable-gzip", "--verbose", "--port=80", "--https-port=443"]
volumes:
- ./wiremock/fake-ghost/mappings:/home/wiremock/mappings

fake-external-activitypub:
image: wiremock/wiremock:3.9.2-1
entrypoint: ["/docker-entrypoint.sh", "--global-response-templating", "--disable-gzip", "--verbose", "--port=80"]
entrypoint: ["/docker-entrypoint.sh", "--global-response-templating", "--disable-gzip", "--verbose", "--port=80", "--https-port=443"]

fake-mastodon:
image: wiremock/wiremock:3.9.2-1
Expand Down
11 changes: 11 additions & 0 deletions features/api/profile.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@api
Feature: Profile
In order to view the details of a profile
As an API user
I want to be able to retrieve a profile

Scenario: Succesfully retreiving a profile
Given an Actor "Alice"
When I request the profile for "Alice"
Then the response has a 200 status code
And the response body contains the profile for "Alice"
11 changes: 11 additions & 0 deletions features/api/search.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@api
Feature: Search
In order to find the details of an actor
As an API user
I want to be able to search for an actor

Scenario: Succesfully searching for an actor
Given an Actor "Alice"
When I search for "Alice"
Then the response has a 200 status code
And the response body contains search results for "Alice"
176 changes: 152 additions & 24 deletions features/step_definitions/stepdefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ async function createActivity(activityType, object, actor, remote = true) {
}
}

function getActorHandle(actor, remote = true) {
return `@${actor.preferredUsername}@${remote ? 'fake-external-activitypub' : 'fake-ghost-activitypub'}`;
}

async function createActor(name = 'Test', remote = true) {
if (remote === false) {
return {
Expand Down Expand Up @@ -118,42 +122,120 @@ async function createActor(name = 'Test', remote = true) {
}
}

// Register endpoints with wiremock - for now just inbox

externalActivityPub.register({
method: 'POST',
endpoint: `/inbox/${name}`
}, {
status: 202
});

return {
const actor = {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/data-integrity/v1',
],
'id': `http://fake-external-activitypub/user/${name}`,
'url': `http://fake-external-activitypub/user/${name}`,
'type': 'Person',
id: `http://fake-external-activitypub/user/${name}`,
url: `http://fake-external-activitypub/user/${name}`,
type: 'Person',

'preferredUsername': name,
'name': name,
'summary': 'A test actor for testing',
preferredUsername: name,
name: name,
summary: 'A test actor for testing',

'inbox': `http://fake-external-activitypub/inbox/${name}`,
'outbox': `http://fake-external-activitypub/inbox/${name}`,
'followers': `http://fake-external-activitypub/followers/${name}`,
'following': `http://fake-external-activitypub/following/${name}`,
inbox: `http://fake-external-activitypub/inbox/${name}`,
outbox: `http://fake-external-activitypub/outbox/${name}`,
followers: `http://fake-external-activitypub/followers/${name}`,
following: `http://fake-external-activitypub/following/${name}`,

'https://w3id.org/security#publicKey': {
'id': 'http://fake-external-activitypub/user#main-key',
'type': 'https://w3id.org/security#Key',
id: 'http://fake-external-activitypub/user#main-key',
type: 'https://w3id.org/security#Key',
'https://w3id.org/security#owner': {
'id': 'http://fake-external-activitypub/user'
id: 'http://fake-external-activitypub/user'
},
'https://w3id.org/security#publicKeyPem': '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtSc3IqGjRaO3vcFdQ15D\nF90WVJC6tb2QwYBh9kQYVlQ1VhBiF6E4GK2okvyvukIL5PHLCgfQrfJmSiopk9Xo\n46Qri6rJbcPoWoZz/jWN0pfmU20hNuTQx6ebSoSkg6rHv1MKuy5LmDGLFC2ze3kU\nsY8u7X6TOBrifs/N+goLaH3+SkT2hZDKWJrmDyHzj043KLvXs/eiyu50M+ERoSlg\n70uO7QAXQFuLMILdy0UNJFM4xjlK6q4Jfbm4MC8QRG+i31AkmNvpY9JqCLqu0mGD\nBrdfJeN8PN+7DHW/Pzspf5RlJtlvBx1dS8Bxo2xteUyLGIaTZ9HZFhHc3IrmmKeW\naQIDAQAB\n-----END PUBLIC KEY-----\n'
}
};

// Mock webfinger
externalActivityPub.register({
method: 'GET',
endpoint: `/.well-known/webfinger?resource=${encodeURIComponent(`acct:${getActorHandle(actor).substring(1)}`)}`
}, {
status: 200,
body: {
links: [
{
rel: "self",
type: "application/activity+json",
href: actor.id
},
]
}
});

// Mock user
externalActivityPub.register({
method: 'GET',
endpoint: `/user/${name}`
}, {
status: 200,
body: actor
});

// Mock followers collection
externalActivityPub.register({
method: 'GET',
endpoint: `/followers/${name}`
}, {
status: 200,
body: {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/data-integrity/v1",
],
id: actor.followers,
type: "OrderedCollection",
orderedItems: []
}
});

// Mock following collection
externalActivityPub.register({
method: 'GET',
endpoint: `/following/${name}`
}, {
status: 200,
body: {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/data-integrity/v1",
],
id: actor.following,
type: "OrderedCollection",
orderedItems: []
}
});

// Mock outbox collection
externalActivityPub.register({
method: 'GET',
endpoint: `/outbox/${name}`
}, {
status: 200,
body: {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/data-integrity/v1",
],
id: actor.outbox,
type: "OrderedCollection",
orderedItems: []
}
});

// Mock inbox
externalActivityPub.register({
method: 'POST',
endpoint: `/inbox/${name}`
}, {
status: 202
});

return actor;
}

function generateObject(type) {
Expand Down Expand Up @@ -309,7 +391,7 @@ Before(async function () {
}
});

async function fetchActivityPub(url, options) {
async function fetchActivityPub(url, options = { method: 'GET' }) {
if (!options.headers) {
options.headers = {};
}
Expand Down Expand Up @@ -663,3 +745,49 @@ Then('a {string} activity is sent to {string}', async function (activityString,

assert(found);
});

When('I request the profile for {string}', async function (name) {
const actor = this.actors[name];

this.response = await fetchActivityPub(`http://fake-ghost-activitypub/.ghost/activitypub/profile/${getActorHandle(actor)}`);
});

Then('the response has a {int} status code', function (statusCode) {
assert.strictEqual(this.response.status, statusCode);
});

Then('the response body contains the profile for {string}', async function (name) {
const result = await this.response.json();
const actor = this.actors[name];

assert.equal(result.actor.id, actor.id);
assert.equal(result.handle, getActorHandle(actor));
assert.equal(result.followerCount, 0);
assert.equal(result.followingCount, 0);
assert.equal(result.isFollowing, false);
assert.deepEqual(result.posts, []);
});

When('I search for {string}', async function (name) {
const actor = this.actors[name];

this.response = await fetchActivityPub(`http://fake-ghost-activitypub/.ghost/activitypub/actions/search?query=${getActorHandle(actor)}`);
});

Then('the response body contains search results for {string}', async function (name) {
const results = await this.response.json();

assert.ok(results.profiles);
assert.equal(results.profiles.length, 1);

assert.ok(results.profiles[0].actor);

const actor = this.actors[results.profiles[0].actor.name];

assert.equal(results.profiles[0].actor.id, actor.id);
assert.equal(results.profiles[0].handle, getActorHandle(actor));
assert.equal(results.profiles[0].followerCount, 0);
assert.equal(results.profiles[0].followingCount, 0);
assert.equal(results.profiles[0].isFollowing, false);
assert.deepEqual(results.profiles[0].posts, []);
});
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ await configure({
filters: {},
loggers: [
{ category: 'activitypub', sinks: ['console'], level: 'info' },
{ category: 'fedify', sinks: ['console'], level: 'warning' }
{ category: 'fedify', sinks: ['console'], level: process.env.NODE_ENV === 'testing' ? 'debug' : 'warning' }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug was useful to figure out why my test wasn't working 😅

],
});

Expand Down
3 changes: 2 additions & 1 deletion src/helpers/activitypub/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,6 @@ export async function isFollowing(actor: Actor, options: {
}

export function isHandle(handle: string): boolean {
return /^@([\w-]+)@([\w-]+\.[\w.-]+)$/.test(handle);
// https://github.com/dahlia/fedify/blob/main/src/vocab/lookup.ts#L29-L30
return /^@?((?:[-A-Za-z0-9._~!$&'()*+,;=]|%[A-Fa-f0-9]{2})+)@([^@]+)$/.test(handle);
}
3 changes: 1 addition & 2 deletions src/helpers/activitypub/actor.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,7 @@ describe('isFollowing', () => {
describe('isHandle', () => {
it('should return a boolean indicating if the provided string is a handle', () => {
expect(isHandle('@[email protected]')).toBe(true);
expect(isHandle('@[email protected]/bar')).toBe(false);
expect(isHandle('@foo@example')).toBe(false);
expect(isHandle('@foo@example')).toBe(true);
expect(isHandle('@example.com')).toBe(false);
expect(isHandle('@foo')).toBe(false);
expect(isHandle('@@foo')).toBe(false);
Expand Down