Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
sefinek committed Aug 15, 2024
1 parent a352cd4 commit 9a3b759
Show file tree
Hide file tree
Showing 9 changed files with 927 additions and 93 deletions.
12 changes: 9 additions & 3 deletions examples/basicUsage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
const NekosiaAPI = require('../index.js');
const { NekosiaAPI } = require('../index.js');

(async () => {
const json = await NekosiaAPI.fetchFoxgirlImages({ session: 'ip', count: 1, additionalTags: ['cute', 'sakura', 'cherry-blossom'], blacklistedTags: ['beret'] });
console.log(json);
const response = await NekosiaAPI.fetchImages('foxgirl', {
session: 'ip',
count: 1,
additionalTags: ['cute', 'sakura', 'cherry-blossom'],
blacklistedTags: ['beret']
});

console.log(response);
})();
11 changes: 3 additions & 8 deletions examples/dynamicCategoryFetch.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
const NekosiaAPI = require('../index.js');
const { NekosiaAPI } = require('../index.js');

const fetchImages = async (category, options = {}) => {
try {
const methodName = `get${category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, '')}`;
if (typeof NekosiaAPI[methodName] !== 'function') {
throw new Error(`Method ${methodName} does not exist on NekosiaAPI`);
}

const images = await NekosiaAPI[methodName](options);
console.log(`${category.toUpperCase()}:`, images);
const response = await NekosiaAPI.fetchImages(category, options);
console.log(`${category.toUpperCase()}:`, response);
} catch (err) {
console.error(`Error fetching ${category} images:`, err);
}
Expand Down
8 changes: 4 additions & 4 deletions examples/fetchShadowImages.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const NekosiaAPI = require('../index.js');
const { NekosiaAPI } = require('../index.js');

(async () => {
const data = await NekosiaAPI.fetchShadowImages(['cute', 'blue-hair'], { session: 'ip', count: 1 });
console.log(data);
})();
const response = await NekosiaAPI.fetchShadowImages({ session: 'ip', count: 1, additionalTags: ['cute', 'blue-hair'] });
console.log(response);
})();
70 changes: 41 additions & 29 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
const https = require('./services/https.js');
const categories = require('./categories.js');
const BASE_URL = 'https://api.nekosia.cat';
const API_URL = `${BASE_URL}/api/v1`;

class NekosiaAPI {
constructor() {
this.baseURL = 'https://api.nekosia.cat/api/v1';
this.initializeCategoryMethods();
}

initializeCategoryMethods() {
categories.forEach(category => {
this[this.buildMethodName(category)] = options => this.fetchImagesByCategory(category, options);
});
}

buildMethodName(category) {
return `fetch${category.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase()).replace(/\s/g, '')}Images`;
}

buildQueryParams(options = {}) {
return Object.entries(options)
.filter(([, value]) => value != null && value !== '' && (!Array.isArray(value) || value.length))
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
.filter(([, value]) => {
if (typeof value === 'string' && value.includes(',')) {
throw new Error('A single tag in the string cannot contain commas. Please use an array instead.');
}

return value != null && value !== '' && (!Array.isArray(value) || value.length > 0);
})
.map(([key, value]) => `${encodeURIComponent(key)}=${value}`)
.join('&');
}

Expand All @@ -33,7 +25,19 @@ class NekosiaAPI {
}
}

fetchImagesByCategory(category, options = {}) {
async fetchImages(category, options = {}) {
if (!category) {
throw new Error('The image category is required. For example, use fetchImages(\'catgirl\').');
}

if (options.session && !['id', 'ip'].includes(options.session)) {
throw new Error('The `session` setting can contain only the following values `id` and `ip`, both as strings.');
}

if (!options.session && options.id) {
throw new Error('`id` is not required if the session is `null` or `undefined`');
}

const queryString = this.buildQueryParams({
session: null,
id: null,
Expand All @@ -42,25 +46,33 @@ class NekosiaAPI {
blacklistedTags: [],
...options
});
return this.makeHttpRequest(`${this.baseURL}/images/${category}?${queryString}`);

return this.makeHttpRequest(`${API_URL}/images/${category}?${queryString}`);
}

async fetchShadowImages(additionalTagsArray = [], options = {}) {
if (!additionalTagsArray.length) {
throw new Error('`additionalTagsArray` must be a non-empty array for the shadow category');
async fetchShadowImages(options = {}) {
if (!Array.isArray(options.additionalTags) || options.additionalTags.length === 0) {
throw new Error('`additionalTags` must be a non-empty array for the shadow category');
}

return this.fetchImagesByCategory('shadow', {
...options,
additionalTags: additionalTagsArray.join(',')
});
return this.fetchImages('shadow', options);
}

async fetchById(id) {
if (!id) throw new Error('`id` parameter is required');

return this.makeHttpRequest(`${this.baseURL}/getImageById/${id}`);
return this.makeHttpRequest(`${API_URL}/getImageById/${id}`);
}
}

module.exports = new NekosiaAPI();
const NekosiaVersion = {
module: https.version,
api: async () => {
return await https.get(BASE_URL);
}
};

module.exports = {
NekosiaAPI: new NekosiaAPI(),
NekosiaVersion
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"license": "ISC",
"author": "Sefinek <[email protected]> (https://sefinek.net)",
"main": "index.js",
"typings": "index.d.ts",
"typings": "types/index.d.ts",
"scripts": {
"test": "jest",
"up": "ncu -u && npm install && npm update && npm audit fix"
Expand Down
4 changes: 2 additions & 2 deletions services/https.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const https = require('https');
const { name, version, devDependencies } = require('../package.json');
const { name, version, homepage, devDependencies } = require('../package.json');

const headers = {
'User-Agent': `Mozilla/5.0 (compatible; ${name}/${version}; +https://nekosia.cat)${process.env.JEST_WORKER_ID ? ` jest/${devDependencies.jest.replace(/^[^0-9]*/, '')}` : ''}`,
'User-Agent': `Mozilla/5.0 (compatible; ${name}/${version}; +${homepage})${process.env.JEST_WORKER_ID ? ` jest/${devDependencies.jest.replace(/^[^0-9]*/, '')}` : ''}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
Expand Down
28 changes: 10 additions & 18 deletions test/api.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const NekosiaAPI = require('../index.js');
const { NekosiaAPI } = require('../index.js');

describe('NekosiaAPI (API Tests)', () => {

describe('fetchImagesByCategory', () => {
describe('fetchImages', () => {
it('should fetch images for the given category', async () => {
const res = await NekosiaAPI.fetchImagesByCategory('catgirl', { count: 1 });
const res = await NekosiaAPI.fetchImages('catgirl', { count: 1 });

expect(res).toBeInstanceOf(Object);
expect(res.success).toBe(true);
Expand Down Expand Up @@ -43,14 +43,14 @@ describe('NekosiaAPI (API Tests)', () => {
});

it('should return an error for an invalid category', async () => {
const res = await NekosiaAPI.fetchImagesByCategory('invalid-category', { count: 1 });
const res = await NekosiaAPI.fetchImages('invalid-category', { count: 1 });

expect(res.success).toBe(false);
expect(res.status).toBe(400);
});

it('should return a specified number of images', async () => {
const res = await NekosiaAPI.fetchImagesByCategory('catgirl', { count: 3 });
const res = await NekosiaAPI.fetchImages('catgirl', { count: 3 });

expect(res).toBeInstanceOf(Object);
expect(res.success).toBe(true);
Expand All @@ -64,18 +64,18 @@ describe('NekosiaAPI (API Tests)', () => {

describe('fetchShadowImages', () => {
it('should handle no images found for shadow category with additional tags', async () => {
const res = await NekosiaAPI.fetchShadowImages(['wTbf8J0TirS6a4fO5uyKcRazZOlO5h6o', 'xX9f9pwDAgsM3Li1LwsJ3tXQfGKW4WA0'], { count: 1 });
const res = await NekosiaAPI.fetchShadowImages({ count: 1, additionalTags: ['wTbf8J0TirS6a4fO5uyKcRazZOlO5h6o', 'xX9f9pwDAgsM3Li1LwsJ3tXQfGKW4WA0'] });

expect(res.success).toBe(false);
expect(res.status).toBe(400);
});

it('should throw an error if additionalTagsArray is empty', async () => {
await expect(NekosiaAPI.fetchShadowImages([])).rejects.toThrow('additionalTagsArray must be a non-empty array for the shadow category.');
await expect(NekosiaAPI.fetchShadowImages([])).rejects.toThrow('`additionalTags` must be a non-empty array for the shadow category');
});

it('should return an error response for invalid count parameter', async () => {
const res = await NekosiaAPI.fetchImagesByCategory('catgirl', { count: 'invalid' });
const res = await NekosiaAPI.fetchImages('catgirl', { count: 'invalid' });
expect(res.success).toBe(false);
expect(res.status).toBe(400);
expect(res.message).toBe('Invalid count parameter. Expected a number between 1 and 48.');
Expand All @@ -84,7 +84,7 @@ describe('NekosiaAPI (API Tests)', () => {

describe('fetchById', () => {
it('should fetch an image by ID if it exists', async () => {
const res = await NekosiaAPI.fetchImagesByCategory('catgirl', { count: 1 });
const res = await NekosiaAPI.fetchImages('catgirl', { count: 1 });

if (res.success && res.id) {
const id = res.id;
Expand All @@ -99,16 +99,8 @@ describe('NekosiaAPI (API Tests)', () => {
}
});

it('should return multiple images for shadow category with valid tags', async () => {
const res = await NekosiaAPI.fetchShadowImages(['catgirl', 'foxgirl'], { count: 7 });
expect(res.status).toBe(200);
expect(res.count).toBe(7);
expect(res.images).toBeInstanceOf(Array);
expect(res.images.length).toBe(7);
});

it('should return an error response for invalid ID format', async () => {
const res = await NekosiaAPI.fetchById(12345);
const res = await NekosiaAPI.fetchById('12345');
expect(res.success).toBe(false);
expect(res.status).toBe(400);
expect(res.message).toBe('The image with the provided identifier was not found.');
Expand Down
46 changes: 18 additions & 28 deletions test/integration.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const api = require('../index.js');
const { NekosiaAPI } = require('../index.js');
const https = require('../services/https.js');
const categories = require('../categories.js');

jest.mock('../services/https.js');

Expand All @@ -9,20 +8,11 @@ describe('NekosiaAPI', () => {
https.get.mockClear();
});

describe('initializeCategoryMethods', () => {
it('should create methods for each category', () => {
categories.forEach(category => {
const methodName = `fetch${category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, '')}Images`;
expect(typeof api[methodName]).toBe('function');
});
});
});

describe('buildQueryParams', () => {
it('should correctly build query params', () => {
const options = { count: 3, additionalTags: 'tag1,tag2', emptyValue: '', nullValue: null };
const result = api.buildQueryParams(options);
expect(result).toBe('count=3&additionalTags=tag1%2Ctag2');
const options = { count: 3, additionalTags: ['tag1', 'tag2', 'tag3', 'tag4'], emptyValue: '', nullValue: null };
const result = NekosiaAPI.buildQueryParams(options);
expect(result).toBe('count=3&additionalTags=tag1,tag2,tag3,tag4');
});
});

Expand All @@ -32,7 +22,7 @@ describe('NekosiaAPI', () => {
https.get.mockResolvedValue(mockResponse);

const endpoint = 'https://api.nekosia.cat/test-endpoint';
const res = await api.makeHttpRequest(endpoint);
const res = await NekosiaAPI.makeHttpRequest(endpoint);

expect(res).toEqual(mockResponse);
expect(https.get).toHaveBeenCalledWith(endpoint);
Expand All @@ -46,37 +36,37 @@ describe('NekosiaAPI', () => {

const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

await expect(api.makeHttpRequest(endpoint)).rejects.toThrow('Request failed');
await expect(NekosiaAPI.makeHttpRequest(endpoint)).rejects.toThrow('Request failed');
expect(https.get).toHaveBeenCalledWith(endpoint);

consoleErrorSpy.mockRestore();
});
});

describe('fetchImagesByCategory', () => {
it('should build correct endpoint and make request', async () => {
describe('fetchImages', () => {
it('should build correct endpoint and make request for given category', async () => {
const mockResponse = { data: { results: [] } };
https.get.mockResolvedValue(mockResponse);

const expectedEndpoint = `${api.baseURL}/api/v1/images/catgirl?count=2&additionalTags=cute`;
const res = await api.fetchImagesByCategory('catgirl', { count: 2, additionalTags: 'cute' });
const expectedEndpoint = 'https://api.nekosia.cat/api/v1/images/catgirl?count=2&additionalTags=cute';
const res = await NekosiaAPI.fetchImages('catgirl', { count: 2, additionalTags: 'cute' });

expect(res).toEqual(mockResponse);
expect(https.get).toHaveBeenCalledWith(expectedEndpoint);
});
});

describe('fetchShadowImages', () => {
it('should throw an error if additionalTagsArray is empty', async () => {
await expect(api.fetchShadowImages([])).rejects.toThrow('additionalTagsArray must be a non-empty array for the shadow category.');
it('should throw an error if additionalTags is empty', async () => {
await expect(NekosiaAPI.fetchShadowImages({})).rejects.toThrow('`additionalTags` must be a non-empty array for the shadow category');
});

it('should correctly call fetchImagesByCategory with additionalTags', async () => {
it('should correctly call fetchImages with additionalTags', async () => {
const mockResponse = { data: { results: [] } };
https.get.mockResolvedValue(mockResponse);

const expectedEndpoint = `${api.baseURL}/api/v1/images/shadow?count=1&additionalTags=dark%2Cshadow`;
const res = await api.fetchShadowImages(['dark', 'shadow'], { count: 1 });
const expectedEndpoint = 'https://api.nekosia.cat/api/v1/images/shadow?count=1&additionalTags=dark,shadow';
const res = await NekosiaAPI.fetchShadowImages({ count: 1, additionalTags: ['dark', 'shadow'] });

expect(res).toEqual(mockResponse);
expect(https.get).toHaveBeenCalledWith(expectedEndpoint);
Expand All @@ -85,18 +75,18 @@ describe('NekosiaAPI', () => {

describe('fetchById', () => {
it('should throw an error if id is not provided', async () => {
await expect(api.fetchById()).rejects.toThrow('id parameter is required.');
await expect(NekosiaAPI.fetchById()).rejects.toThrow('`id` parameter is required');
});

it('should correctly fetch image by ID', async () => {
const mockResponse = { data: { id: '123' } };
https.get.mockResolvedValue(mockResponse);

const id = '123';
const res = await api.fetchById(id);
const res = await NekosiaAPI.fetchById(id);

expect(res).toEqual(mockResponse);
expect(https.get).toHaveBeenCalledWith(`${api.baseURL}/api/v1/getImageById/${id}`);
expect(https.get).toHaveBeenCalledWith(`https://api.nekosia.cat/api/v1/getImageById/${id}`);
});
});
});
Loading

0 comments on commit 9a3b759

Please sign in to comment.