Add network protection and administrator account support

This commit is contained in:
2026-10-15 12:05:52 +09:00
parent 471e2653d8
commit 4d527f74e6
20 changed files with 2913 additions and 106 deletions

View File

@@ -0,0 +1,349 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { eq } from 'drizzle-orm';
import { fixture } from './helpers.js';
import * as s from '../src/db/schema.js';
import { auditChangeLines, formatCountry } from '../../web/src/displayFormat.js';
async function account(f, email) {
const c = await f.create();
f.service.db
.insert(s.users)
.values({
id: randomUUID(),
email,
displayName: email,
passwordHash: await f.service.auth.password('12345678'),
status: 'active',
verifiedAt: Date.now(),
createdAt: Date.now(),
})
.run();
const response = await f.call(c, '/auth/login', 'POST', { email, password: '12345678' });
assert.equal(response.statusCode, 200, response.body);
c.cookie += '; ' + String(response.headers['set-cookie']).split(';')[0];
return { ...c, userId: response.json().account.id, email };
}
const h = { 'cf-connecting-ip': '8.8.4.4', 'cf-ipcountry': 'KR', 'cf-ipcity': 'Daejeon' };
test(
'audit shows only differences, skips no-op saves, compares compatible old entries and supports individual/all deletion with authorization',
{ timeout: 20000 },
async () => {
const f = await fixture();
try {
const admin = await account(f, 'hello@noname.company'),
user = await account(f, 'user@hop.test');
let response = await f.call(admin, '/admin/settings', 'PATCH', {
revision: 0,
maxConcurrentUploads: 5,
});
assert.equal(response.statusCode, 200);
response = await f.call(admin, '/admin/settings', 'PATCH', {
revision: 1,
maxConcurrentUploads: 10,
blockedCountries: ['CN'],
});
assert.equal(response.statusCode, 200);
const events = (await f.call(admin, '/admin/audit')).json().events;
const latest = events.find((e) => e.details.changes?.some((c) => c.after === 10));
assert.deepEqual(latest.details.changes, [
{ field: 'maxConcurrentUploads', before: 5, after: 10 },
{ field: 'blockedCountries', added: ['CN'], removed: [] },
]);
assert.deepEqual(latest.details.changes.flatMap(auditChangeLines), [
'동시에 처리할 업로드 수 5 > 10',
'블랙리스트 추가: 중국(CN)',
]);
const count = events.length;
await f.call(admin, '/admin/settings', 'PATCH', { revision: 2, maxConcurrentUploads: 10 });
assert.equal((await f.call(admin, '/admin/audit')).json().events.length, count);
assert.equal(
(await f.call(user, '/admin/audit/remove', 'POST', { all: true })).statusCode,
403,
);
assert.equal(
(await f.call(admin, '/admin/audit/remove', 'POST', { ids: [latest.id] })).statusCode,
200,
);
assert.equal(
(await f.call(admin, '/admin/audit')).json().events.some((e) => e.id === latest.id),
false,
);
const before = {
uploadsPaused: false,
maxConcurrentUploads: 5,
blockedCountries: [],
blockedIps: [],
totalStorageLimitMb: 20480,
};
for (const [offset, body] of [
[2000, before],
[1000, { ...before, maxConcurrentUploads: 6 }],
])
f.service.db
.insert(s.adminAudit)
.values({
id: randomUUID(),
actorUserId: admin.userId,
action: 'settings-change',
details: JSON.stringify(body),
createdAt: Date.now() - offset,
})
.run();
const old = (await f.call(admin, '/admin/audit'))
.json()
.events.find((e) => e.details.legacy && e.details.changes.length);
assert.deepEqual(old.details.changes, [
{ field: 'maxConcurrentUploads', before: 5, after: 6 },
]);
assert.equal(
(await f.call(admin, '/admin/audit/remove', 'POST', { all: true })).statusCode,
200,
);
assert.deepEqual((await f.call(admin, '/admin/audit')).json().events, []);
assert.equal(
f.service.admin.state.maxConcurrentUploads,
10,
'deleting audit entries never changes settings',
);
assert.equal(formatCountry('JP'), '일본(JP)');
} finally {
await f.close();
}
},
);
test(
'user list keeps basic last-seen country/IP with throttled updates, expires location data and never exposes credentials',
{ timeout: 20000 },
async () => {
const f = await fixture();
try {
const admin = await account(f, 'hello@noname.company'),
user = await account(f, 'user@hop.test'),
guest = await f.create();
f.cfg.clientIpSource = 'cloudflare';
await f.call(user, '/auth/me', 'GET', undefined, undefined, h);
const first = f.service.db
.select()
.from(s.userActivity)
.where(eq(s.userActivity.userId, user.userId))
.get();
await f.call(user, '/auth/me', 'GET', undefined, undefined, h);
assert.equal(
f.service.db
.select()
.from(s.userActivity)
.where(eq(s.userActivity.userId, user.userId))
.get().lastSeenAt,
first.lastSeenAt,
);
const response = await f.call(
admin,
'/admin/users?q=user@hop.test',
'GET',
undefined,
undefined,
h,
),
list = response.json().users;
assert.equal(list.length, 1);
assert.equal(list[0].email, user.email);
assert.equal(list[0].countryCode, 'KR');
assert.equal(list[0].city, 'Daejeon');
assert.equal(list[0].ip, '8.8.4.4');
assert.equal(response.body.includes('passwordHash'), false);
assert.equal(response.body.includes('tokenHash'), false);
assert.equal((await f.call(user, '/admin/users')).statusCode, 403);
assert.equal((await f.call(guest, '/admin/users')).statusCode, 401);
f.service.db
.update(s.userActivity)
.set({ lastSeenAt: Date.now() - 31 * 86400000 })
.where(eq(s.userActivity.userId, user.userId))
.run();
const expired = (
await f.call(admin, '/admin/users?q=user@hop.test', 'GET', undefined, undefined, h)
).json().users[0];
assert.equal(expired.ip, null);
assert.equal(expired.countryCode, null);
assert.ok(expired.lastSeenAt);
f.service.admin.cleanup();
assert.equal(
f.service.db
.select()
.from(s.userActivity)
.where(eq(s.userActivity.userId, user.userId))
.get().city,
null,
);
} finally {
await f.close();
}
},
);
test(
'admin reset sends a one-use email link; password and sessions change only when the user completes it',
{ timeout: 20000 },
async () => {
const mail = [];
const f = await fixture(false, async (message) => mail.push(message));
try {
const admin = await account(f, 'hello@noname.company'),
user = await account(f, 'user@hop.test');
assert.equal(
(await f.call(user, `/admin/users/${admin.userId}/password-reset`, 'POST', {})).statusCode,
403,
);
assert.equal(
(await f.call(admin, `/admin/users/${user.userId}/password-reset`, 'POST', {})).statusCode,
200,
);
assert.equal(mail.length, 1);
assert.equal(mail[0].to, user.email);
assert.ok((await f.call(user, '/auth/me')).json().account);
const token = /#reset-password=([^\s]+)/.exec(mail[0].text)[1];
const reset = await f.call(user, '/auth/reset-password', 'POST', {
token,
password: 'new-password',
passwordConfirmation: 'new-password',
});
assert.equal(reset.statusCode, 200, reset.body);
assert.equal((await f.call(user, '/auth/me')).json().account, null);
const guest = await f.create();
assert.equal(
(await f.call(guest, '/auth/login', 'POST', { email: user.email, password: '12345678' }))
.statusCode,
401,
);
assert.equal(
(
await f.call(guest, '/auth/login', 'POST', {
email: user.email,
password: 'new-password',
})
).statusCode,
200,
);
assert.equal(
(
await f.call(guest, '/auth/reset-password', 'POST', {
token,
password: 'again-password',
passwordConfirmation: 'again-password',
})
).statusCode,
400,
);
} finally {
await f.close();
}
},
);
test(
'admin deletion confirms email, protects the administrator, revokes credentials and preserves shared recipient files',
{ timeout: 20000 },
async () => {
const f = await fixture();
try {
const admin = await account(f, 'hello@noname.company'),
user = await account(f, 'user@hop.test'),
recipient = await f.create();
const created = await f.call(
user,
'/vault',
'POST',
{ kind: 'file', files: [{ name: 'shared.txt', size: 4, type: 'text/plain' }] },
randomUUID(),
);
assert.equal(created.statusCode, 200, created.body);
const id = created.json().requestId,
payload = Buffer.from(
'--delete\r\nContent-Disposition: form-data; name="file"; filename="shared.txt"\r\nContent-Type: text/plain\r\n\r\ntest\r\n--delete--\r\n',
);
const uploaded = await f.call(
user,
`/uploads?requestId=${id}&fileIndex=0`,
'POST',
payload,
randomUUID(),
{ 'content-type': 'multipart/form-data; boundary=delete' },
);
assert.equal(uploaded.statusCode, 200, uploaded.body);
const file = f.service.storage.fileRows(id)[0],
item = f.service.db.select().from(s.items).where(eq(s.items.fileId, file.id)).get(),
accessId = randomUUID();
f.service.db
.insert(s.access)
.values({
id: accessId,
itemId: item.id,
guestId: f.service.deviceGuest(recipient.deviceId).guestId,
role: 'recipient',
read: false,
})
.run();
assert.equal(
(await f.call(admin, `/admin/users/${admin.userId}/delete`, 'POST', { email: admin.email }))
.statusCode,
403,
);
assert.equal(
(
await f.call(admin, `/admin/users/${user.userId}/delete`, 'POST', {
email: 'wrong@hop.test',
})
).statusCode,
409,
);
assert.equal(
(await f.call(user, `/admin/users/${user.userId}/delete`, 'POST', { email: user.email }))
.statusCode,
403,
);
assert.equal(
(await f.call(admin, `/admin/users/${user.userId}/delete`, 'POST', { email: user.email }))
.statusCode,
200,
);
const deleted = f.service.auth.user(user.userId);
assert.equal(deleted.status, 'deleted');
assert.equal(deleted.email, null);
assert.equal(deleted.passwordHash, null);
assert.equal((await f.call(user, '/auth/me')).json().account, null);
assert.equal(
(await f.call(admin, '/admin/users')).json().users.some((u) => u.id === user.userId),
false,
);
const download = await f.call(recipient, `/access/${accessId}/download`);
assert.equal(download.statusCode, 200, download.body);
assert.equal(download.body, 'test');
assert.equal((await f.call(admin, '/admin/audit')).json().events[0].action, 'account-delete');
} finally {
await f.close();
}
},
);
test(
'account deletion during password verification cannot issue a usable new login session',
{ timeout: 20000 },
async () => {
const f = await fixture();
try {
const user = await account(f, 'race@hop.test');
const login = f.service.auth.login(user.email, '12345678', '8.8.8.8');
f.service.db
.update(s.users)
.set({ status: 'deleted', email: null, passwordHash: null })
.where(eq(s.users.id, user.userId))
.run();
await assert.rejects(login, (error) => error.code === 'LOGIN_FAILED');
} finally {
await f.close();
}
},
);

View File

@@ -0,0 +1,487 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import { eq } from 'drizzle-orm';
import { io } from 'socket.io-client';
import { fixture } from './helpers.js';
import * as s from '../src/db/schema.js';
import { Administration } from '../src/administration.js';
const actorHeaders = { 'cf-connecting-ip': '8.8.4.4', 'cf-ipcountry': 'KR' };
const headers = (ip = '8.8.8.8', country = 'KR') => ({
'cf-connecting-ip': ip,
...(country ? { 'cf-ipcountry': country } : {}),
});
async function login(f, email = 'hello@noname.company') {
const c = await f.create();
f.service.db
.insert(s.users)
.values({
id: randomUUID(),
email,
displayName: email,
passwordHash: await f.service.auth.password('12345678'),
status: 'active',
verifiedAt: Date.now(),
createdAt: Date.now(),
})
.run();
const response = await f.call(c, '/auth/login', 'POST', { email, password: '12345678' });
assert.equal(response.statusCode, 200, response.body);
c.cookie += '; ' + String(response.headers['set-cookie']).split(';')[0];
return c;
}
async function patch(f, admin, body) {
const response = await f.call(
admin,
'/admin/settings',
'PATCH',
{ revision: f.service.admin.state.revision, ...body },
undefined,
actorHeaders,
);
assert.equal(response.statusCode, 200, response.body);
return response.json();
}
async function autoFixture(settings = {}) {
const f = await fixture(),
admin = await login(f),
guest = await f.create();
f.cfg.clientIpSource = 'cloudflare';
await patch(f, admin, {
autoBlockEnabled: true,
autoLoginFailures: 3,
autoRateViolations: 3,
...settings,
});
return { f, admin, guest };
}
test(
'existing saved settings retain blacklist rules while new whitelist and automatic protection default to empty/off',
{ timeout: 20000 },
async () => {
const f = await fixture();
try {
const legacy = {
revision: 4,
uploadsPaused: false,
maxConcurrentUploads: 3,
totalStorageLimitMb: 12000,
blockedCountries: ['JP'],
blockedIps: ['1.1.1.0/24'],
};
f.service.db
.insert(s.adminSettings)
.values({ name: 'controls', value: JSON.stringify(legacy), updatedAt: Date.now() })
.run();
const restored = new Administration(f.service);
assert.equal(restored.state.revision, 4);
assert.deepEqual(restored.state.blockedCountries, ['JP']);
assert.equal(restored.state.blacklistEnabled, true);
assert.equal(restored.state.autoBlockEnabled, false);
assert.deepEqual(restored.state.allowedCountries, []);
assert.deepEqual(restored.state.allowedIps, []);
} finally {
await f.close();
}
},
);
test(
'whitelist is an OR of countries and IP ranges, excludes unknown origins, preserves blacklist priority and prevents operator lockout',
{ timeout: 20000 },
async () => {
const f = await fixture();
try {
const admin = await login(f),
guest = await f.create();
f.cfg.clientIpSource = 'cloudflare';
await patch(f, admin, {
allowedCountries: ['KR'],
allowedIps: ['1.1.1.0/24', '2001:db8::/32'],
blockedIps: ['1.1.1.5'],
});
for (const [ip, country, status] of [
['8.8.8.8', 'KR', 200],
['1.1.1.6', 'JP', 200],
['9.9.9.9', 'JP', 403],
['1.1.1.5', 'JP', 403],
['9.9.9.9', null, 403],
])
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers(ip, country)))
.statusCode,
status,
);
assert.equal(f.service.admin.policyDenied({ ip: '2001:db8::123', countryCode: null }), false);
assert.equal(f.service.admin.policyDenied({ ip: '2001:db9::123', countryCode: null }), true);
const denied = await f.call(
admin,
'/admin/settings',
'PATCH',
{ revision: f.service.admin.state.revision, allowedCountries: ['JP'], allowedIps: [] },
undefined,
actorHeaders,
);
assert.equal(denied.json().code, 'ADMIN_SELF_BLOCK');
assert.deepEqual(f.service.admin.state.allowedCountries, ['KR']);
assert.equal(
(await f.call(null, '/health')).statusCode,
200,
'local container health checks survive a country-only whitelist',
);
assert.equal(
(await f.call(guest, '/health', 'GET', undefined, undefined, headers('9.9.9.9', 'JP')))
.statusCode,
403,
'proxied public health requests still follow the whitelist',
);
await patch(f, admin, { blacklistEnabled: false });
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers('1.1.1.5', 'JP')))
.statusCode,
200,
);
await patch(f, admin, { allowedCountries: [], allowedIps: [] });
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers('9.9.9.9', null)))
.statusCode,
200,
);
const invalid = await f.call(
admin,
'/admin/network-rules/validate',
'POST',
{ kind: 'ip', value: '1.1.1.999/24' },
undefined,
actorHeaders,
);
assert.equal(invalid.statusCode, 400);
const normalized = await f.call(
admin,
'/admin/network-rules/validate',
'POST',
{ kind: 'country', value: 'jp' },
undefined,
actorHeaders,
);
assert.deepEqual(normalized.json(), { kind: 'country', value: 'JP' });
assert.equal((await f.call(guest, '/admin/automatic-blocks')).statusCode, 401);
assert.equal(
(
await f.call(guest, '/admin/network-rules/validate', 'POST', {
kind: 'country',
value: 'KR',
})
).statusCode,
401,
);
} finally {
await f.close();
}
},
);
test(
'activating a whitelist disconnects existing excluded WebSockets and denies a new handshake',
{ timeout: 20000 },
async () => {
const f = await fixture();
let socket;
try {
const admin = await login(f),
guest = await f.create();
f.cfg.clientIpSource = 'cloudflare';
socket = io(f.url, {
transports: ['websocket'],
auth: { csrfToken: guest.csrf },
extraHeaders: {
Origin: 'http://hop.test',
Cookie: guest.cookie,
...headers('1.1.1.1', 'JP'),
},
reconnection: false,
});
await new Promise((resolve, reject) => {
socket.once('connect', resolve);
socket.once('connect_error', reject);
});
const disconnected = new Promise((resolve) => socket.once('disconnect', resolve));
await patch(f, admin, { allowedCountries: ['KR'] });
await disconnected;
assert.equal(socket.connected, false);
const failed = new Promise((resolve) => socket.once('connect_error', resolve));
socket.connect();
await failed;
assert.equal(socket.connected, false);
} finally {
socket?.disconnect();
await f.close();
}
},
);
test(
'automatic login bans persist, can be recovered by an existing verified admin, and unblock/expiry restore access',
{ timeout: 20000 },
async () => {
const { f, admin, guest } = await autoFixture();
try {
for (let i = 0; i < 3; i++) {
const response = await f.call(
guest,
'/auth/login',
'POST',
{ email: 'missing@hop.test', password: 'incorrect' },
undefined,
headers(),
);
assert.equal(response.json().code, 'LOGIN_FAILED');
}
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers())).statusCode,
403,
);
assert.equal(new Administration(f.service).automatic.has('8.8.8.8'), true);
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers('1.1.1.1')))
.statusCode,
200,
);
assert.equal(
(await f.call(admin, '/admin/summary', 'GET', undefined, undefined, headers())).statusCode,
200,
'admin recovery session remains usable',
);
const list = (
await f.call(
admin,
'/admin/automatic-blocks?q=8.8',
'GET',
undefined,
undefined,
actorHeaders,
)
).json();
assert.equal(list.total, 1);
assert.equal(list.items[0].reason, 'login-failures');
const removal = await f.call(
admin,
'/admin/automatic-blocks/remove',
'POST',
{ ips: ['8.8.8.8'] },
undefined,
actorHeaders,
);
assert.equal(removal.statusCode, 200);
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers())).statusCode,
200,
);
const fake = { raw: { socket: { remoteAddress: '127.0.0.1' } }, headers: headers() };
for (let i = 0; i < 3; i++) f.service.admin.automatic.observe(fake, 'login');
f.service.admin.automatic.active.get('8.8.8.8').expiresAt = Date.now() - 1;
f.service.db
.update(s.automaticBlocks)
.set({ expiresAt: Date.now() - 1 })
.run();
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers())).statusCode,
200,
);
f.service.admin.automatic.prune();
assert.equal(f.service.db.select().from(s.automaticBlocks).all().length, 0);
await patch(f, admin, { blockedIps: ['8.8.8.8'] });
assert.equal(
(await f.call(admin, '/admin/summary', 'GET', undefined, undefined, headers())).statusCode,
403,
'admin sessions never bypass manual blacklist',
);
} finally {
await f.close();
}
},
);
test(
'repeated file attempts and declared byte budgets cover idempotent retries and multiple guest sessions on the same IP',
{ timeout: 20000 },
async () => {
const { f, admin, guest } = await autoFixture({ autoUploadRequests: 3 });
try {
const receiver = await f.create();
await f.space(guest, receiver);
const input = {
targetDeviceId: receiver.deviceId,
kind: 'file',
count: 1,
byteSize: 4,
files: [{ name: 'a.txt', size: 4, type: 'text/plain' }],
},
key = randomUUID();
const first = await f.call(guest, '/transfer-requests', 'POST', input, key, headers());
assert.equal(first.statusCode, 200, first.body);
for (let i = 0; i < 4; i++)
assert.equal(
(await f.call(guest, '/transfer-requests', 'POST', input, key, headers())).statusCode,
200,
);
assert.equal(f.service.admin.automatic.counters.get('8.8.8.8|upload').count, 1);
await f.call(guest, '/transfer-requests', 'POST', input, randomUUID(), headers());
const other = await f.create();
await f.call(other, '/transfer-requests', 'POST', input, randomUUID(), headers());
const blocked = await f.call(
other,
'/transfer-requests',
'POST',
input,
randomUUID(),
headers(),
);
assert.equal(blocked.statusCode, 403);
assert.equal(f.service.admin.automatic.active.get('8.8.8.8').reason, 'file-requests');
await f.call(
admin,
'/admin/automatic-blocks/remove',
'POST',
{ ips: ['8.8.8.8'] },
undefined,
actorHeaders,
);
await patch(f, admin, { autoUploadRequests: 30, autoUploadBytes: 1000000 });
const huge = {
...input,
byteSize: 1000001,
files: [{ name: 'a.mp4', size: 1000001, type: 'video/mp4' }],
};
const volume = await f.call(
other,
'/transfer-requests',
'POST',
huge,
randomUUID(),
headers(),
);
assert.equal(volume.statusCode, 403);
assert.equal(f.service.admin.automatic.active.get('8.8.8.8').reason, 'file-volume');
assert.equal(f.service.storage.fileRows(first.json().requestId).length, 1);
} finally {
await f.close();
}
},
);
test(
'automatic rate protection counts deliberate limit violations but ignores normal chunk requests and untrusted origin headers',
{ timeout: 20000 },
async () => {
const { f, guest } = await autoFixture();
try {
for (let i = 0; i < 240; i++) f.service.limiter.hit('api-ip:8.8.8.8', 240);
for (let i = 0; i < 3; i++)
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers())).statusCode,
429,
);
assert.equal(f.service.admin.automatic.active.get('8.8.8.8').reason, 'rate-violations');
const fake = { raw: { socket: { remoteAddress: '10.0.0.2' } }, headers: headers('1.1.1.1') };
for (let i = 0; i < 4; i++)
assert.equal(f.service.admin.automatic.observe(fake, 'login'), false);
assert.equal(f.service.admin.automatic.has('1.1.1.1'), false);
for (let i = 0; i < 6000; i++) f.service.limiter.hit('chunk-ip:9.9.9.9', 6000);
for (let i = 0; i < 4; i++)
assert.equal(
(
await f.call(
guest,
`/uploads/resumable/${randomUUID()}`,
'PUT',
Buffer.from('x'),
undefined,
{ 'content-type': 'application/octet-stream', ...headers('9.9.9.9') },
)
).statusCode,
429,
);
assert.equal(f.service.admin.automatic.has('9.9.9.9'), false);
} finally {
await f.close();
}
},
);
test(
'signed-in vault file requests share the same IP byte budget and deduplicate retry keys',
{ timeout: 20000 },
async () => {
const { f, guest } = await autoFixture({ autoUploadBytes: 1000000 });
try {
const user = await login(f, 'user@hop.test'),
key = randomUUID(),
body = { kind: 'file', files: [{ name: 'vault.txt', size: 600000, type: 'text/plain' }] };
const first = await f.call(user, '/vault', 'POST', body, key, headers());
assert.equal(first.statusCode, 200, first.body);
assert.equal((await f.call(user, '/vault', 'POST', body, key, headers())).statusCode, 200);
const second = await f.call(
user,
'/vault',
'POST',
{ kind: 'file', files: [{ name: 'second.txt', size: 500000, type: 'text/plain' }] },
randomUUID(),
headers(),
);
assert.equal(second.statusCode, 403);
assert.equal(f.service.admin.automatic.active.get('8.8.8.8').reason, 'file-volume');
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers())).statusCode,
403,
);
} finally {
await f.close();
}
},
);
test(
'successful login resets only login failures, automatic disable preserves recoverable entries, and reenabling applies them',
{ timeout: 20000 },
async () => {
const { f, admin, guest } = await autoFixture();
try {
await login(f, 'success@hop.test');
const req = { raw: { socket: { remoteAddress: '127.0.0.1' } }, headers: headers() };
f.service.admin.automatic.observe(req, 'login');
f.service.admin.automatic.observe(req, 'rate');
assert.equal(
(
await f.call(
guest,
'/auth/login',
'POST',
{ email: 'success@hop.test', password: '12345678' },
undefined,
headers(),
)
).statusCode,
200,
);
assert.equal(f.service.admin.automatic.counters.has('8.8.8.8|login'), false);
assert.equal(f.service.admin.automatic.counters.get('8.8.8.8|rate').count, 1);
for (let i = 0; i < 3; i++) f.service.admin.automatic.observe(req, 'login');
await patch(f, admin, { autoBlockEnabled: false });
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers())).statusCode,
200,
);
assert.equal(f.service.admin.automatic.has('8.8.8.8'), true);
await patch(f, admin, { autoBlockEnabled: true });
assert.equal(
(await f.call(guest, '/device/me', 'GET', undefined, undefined, headers())).statusCode,
403,
);
} finally {
await f.close();
}
},
);