Setup test environment for Strapi project - node.js

I am trying to setup unit test for a Strapi project my code looks like below
test_utils.js
const Strapi = require("strapi");
const http = require('http');
let instance; // singleton
jest.setTimeout(10000)
async function setupStrapi() {
if (!instance) {
instance = Strapi()
await instance.load();
// Run bootstrap function.
await instance.runBootstrapFunctions();
// Freeze object.
await instance.freeze();
instance.app.use(instance.router.routes()).use(instance.router.allowedMethods());
instance.server = http.createServer(instance.app.callback());
}
return instance;
}
module.exports = { setupStrapi }
controllers.test.js
const request = require("supertest")
const {setupStrapi, setupUser} = require("../../test_utils")
describe("chat-group controllers", ()=>{
let strapi
beforeAll(async ()=>{
strapi = await setupStrapi()
})
test("endpoint tasks", async (done)=>{
app = strapi
app.server.listen(app.config.port, app.config.host)
const resp = await request(app.server).get("/testpublics")
.expect(200)
console.log(resp.body)
done()
})
})
when I run the test, I get 403 error on "/testpublics". Note that "/testpublics" is public api and I can access it from browser.
I think the problem is with setupStrapi function, I took the code from node_modules/strapi/lib/strapi.js file.
What is the better way to setup unit test for Strapi project. I want to achieve following
start test with clean database each time
test public and authenticated api endpoints

I encountered the same problem. Go to ./api/name-of-your-api/config/routes.json and remove the config property for each of the endpoints.
It should be this:
{
"routes": [
{
"method": "GET",
"path": "/testpublics",
"handler": "testpublics.index"
},
}
as opposed to this:
{
"routes": [
{
"method": "GET",
"path": "/testpublics",
"handler": "testpublics.index",
"config": {
"policies": []
}
},
}

If you want this route to by public by policy, answer from #sama-bala resolves everything.
For Strapi the custom route and controller that is not public (needs JWT token in Request header) must be assigned to role — otherwise even for a valid token controller will throw Forbidden 403 error. The whole process is described in Authenticated request tutorial on Strapi documentation page. This information is saved in the database only. Usually you do this in the admin panel, not from source code.
Have a look on the following snippet
/**
* Grants database `permissions` table that role can access an endpoint/controllers
*
* #param {int} roleID, 1 Autentihected, 2 Public, etc
* #param {string} value, in form or dot string eg `"permissions.users-permissions.controllers.auth.changepassword"`
* #param {boolean} enabled, default true
* #param {string} policy, default ''
*/
const grantPrivilage = async (
roleID = 1,
value,
enabled = true,
policy = ""
) => {
const updateObj = value
.split(".")
.reduceRight((obj, next) => ({ [next]: obj }), { enabled, policy });
return await strapi.plugins[
"users-permissions"
].services.userspermissions.updateRole(roleID, updateObj);
};
It allows you to assign route to role programatically by updating the database. In case of your code the solution might look like that
await grantPrivilage(2, "permissions.application.controllers.testpublics.index"); // 1 is default role for Autheticated user, 2 is Public role.
You can add this in beforeAll or in bootstrap.js, eg
beforeAll(async (done) => {
user = await userFactory.createUser(strapi);
await grantPrivilage(1, "permissions.application.controllers.hello.hi");
done();
});
I've tried to explore this topic in my blog post

Related

Node.js Google Sheets API: can read but not create spreadsheet

I have been working all day to try to get a Node.js application connected to my Google Drive to programmatically create spreadsheets using the Google Sheets API.
I think I have set up my connection correctly, because the following code block executes correctly:
/**
* Load or request or authorization to call APIs.
*
*/
async function authorize() {
let client = await loadSavedCredentialsIfExist();
if (client) {
return client;
}
client = await authenticate({
scopes: SCOPES,
keyfilePath: CREDENTIALS_PATH,
});
if (client.credentials) {
await saveCredentials(client);
}
return client;
}
/**
* Prints the names and majors of students in a sample spreadsheet:
* #see https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
* #param {google.auth.OAuth2} auth The authenticated Google OAuth client.
*/
async function listMajors(auth) {
const sheets = google.sheets({ version: 'v4', auth });
const res = await sheets.spreadsheets.values.get({
spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
range: 'Class Data!A2:E',
});
const rows = res.data.values;
if (!rows || rows.length === 0) {
console.log('No data found.');
return;
}
console.log('Name, Major:');
rows.forEach((row) => {
// Print columns A and E, which correspond to indices 0 and 4.
console.log(`${row[0]}, ${row[4]}`);
});
}
authorize().then(listMajors).catch(console.error);
As soon as I move from that code to the following code, I get 403 Insufficient Permission errors:
/**
* Create a google spreadsheet
* #param {string} title Spreadsheets title
* #return {string} Created spreadsheets ID
*/
async function create(title) {
const { GoogleAuth } = require('google-auth-library');
const { google } = require('googleapis');
const auth = new GoogleAuth({
scopes: 'https://www.googleapis.com/auth/spreadsheets',
});
const service = google.sheets({ version: 'v4', auth });
const resource = {
properties: {
title,
},
};
try {
const spreadsheet = await service.spreadsheets.create({
resource,
fields: 'spreadsheetId',
});
console.log(`Spreadsheet ID: ${spreadsheet.data.spreadsheetId}`);
return spreadsheet.data.spreadsheetId;
} catch (err) {
// TODO (developer) - Handle exception
throw err;
}
}
authorize().then(create).catch(console.error);
I have tried this using OAuth2 Client ID, Service Account, and Application Default Credentials. I have enabled all Scopes for my application. I really don't understand what else it wants me to configure to tell it I can have access.
I am attempting to access an application created using my personal Gmail address by logging in using my personal Gmail address. When trying to use a Service Account, I created a new folder in Drive and gave it permissions.
I do not see any additional info in the Create a spreadsheet tutorial.
What other permission is it expecting me to grant and where?
In my case, the application of my scopes took quite a bit longer than I anticipated. Once they were added to my application, the code worked correctly. I would estimate that it took 4-5 hours for the updates to finally show up.

Calling (private) cloud function from cloud function

I keep getting a 403 with the testcode below. Is it just me or is it overly complicated to call a function within the same project? I did some research here and here.
I've set the cloud function invoker on the default service account for both functions. And the allow internal traffic
So i have tried both codes below. The token is printed to the logs in the first function, so why do i still get a 403?
Script1:
const axios = require("axios");
/**
* Responds to any HTTP request.
*
* #param {!express:Request} req HTTP request context.
* #param {!express:Response} res HTTP response context.
*/
exports.helloWorld = async (req, res) => {
console.log(JSON.stringify(process.env));
const sample_api_url = `https://someRegionAndSomeProject.cloudfunctions.net/sample-api`;
const metadataServerURL =
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=";
const tokenUrl = metadataServerURL + sample_api_url;
// Fetch the token
const tokenResponse = await axios(tokenUrl, {
method: "GET",
headers: {
"Metadata-Flavor": "Google",
},
});
const token = tokenResponse.data;
console.log(token);
const functionResponse = await axios(sample_api_url, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
const data = functionResponse.data;
console.log(data);
res.status(200).json({ token, data });
};
Script2:
const {GoogleAuth} = require('google-auth-library');
/**
* Responds to any HTTP request.
*
* #param {!express:Request} req HTTP request context.
* #param {!express:Response} res HTTP response context.
*/
exports.helloWorld = async (req, res) => {
const url = 'https://someRegionAndSomeProject.cloudfunctions.net/sample-api';
const targetAudience = url;
const auth = new GoogleAuth();
const client = await auth.getIdTokenClient(targetAudience);
const response = await client.request({url});
res.status(200).json({data: response.data})
};
There is 2 types of security on Cloud Functions
Identity based security: if you deploy your Cloud Functions without the allow-unauthenticated parameter, you have to send a request with an Authorization: bearer <token> header, with token is an identity token which has, at least, the cloud functions invoker role. You can also add the allUsers user with the cloud functions invoker role to make the function publicly reachable (no security header required)
Network based security: this time, only the request coming from your project VPCs or your VPC SC are allowed to access the Cloud Functions. If you try to reach the cloud functions from an unauthorized network, you get a 403 (your error).
You can combine the 2 security solutions if you want. In your code, you correctly add the security header. However, your request is rejected by the network check.
The solution is not so simple, not free, and I totally agree with you that this pattern should be simpler.
To achieve this, you must create a serverless VPC connector and attach it on your functions that perform the call. You also have to set the egress to ALL on that functions. That's all
The consequence are the following: The traffic originated from your function will be routed to your VPC thanks to the serverless VPC connector. The Cloud Functions URL being always a public URL, you have to set the egress to ALL, to route the traffic going to public URL through the serverless VPC connector.
Based on your post, I created a sample that worked for me. I created two GCP functions "func1" and "func2". I then determined how to call func2 from func1 where func2 is only exposed to be invoked by the service account identity which func1 runs as.
The final code for func1 is as follows:
const func2Url = 'https://us-central1-XXX.cloudfunctions.net/func2';
const targetAudience = func2Url;
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
async function request() {
console.info(`request ${func2Url} with target audience ${targetAudience}`);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url: func2Url});
console.info(res.data);
return res;
}
exports.func1 = async (req, res) => {
let message = `Hello from func1`;
try {
let response = await request();
res.status(200).send(`${message} + ${response.data}`);
}
catch(e) {
console.log(e);
res.status(500).send('failed');
}
};
The primary recipe used here is as described in the Google Docs here. I also found this medium article helpful.

TikTok oAuth API auth code is always expired

I am trying to login using TikTok oAuth API
I have a Firebase Cloud Function (Nodejs) set up to complete the oauth flow, based
on the TikTok API Documentation, but when i reach the point (https://open-api.tiktok.com/oauth/access_token) to get the actual user access token it fails and i get an error.
The response i get is status 200 and
{
"data": {
"captcha": "",
"desc_url": "",
"description": "Authorization code expired",
"error_code": 10007
},
"message": "error"
}
The TikTok API always gives me the same authorization code. So i am guessing something is wrong. Any suggestion is welcomed.
Here is the code sample from the backend
The /linkTikTok/oauth and point used to redirect the user to tikTok oauth and the /linkTikTok/validate is used to request the access token. The code runs fine but when it reaches const URL = https://open-api.tiktok.com/oauth/access_token; and actually requests the user access token i get response above.
import * as express from 'express';
import * as cors from 'cors';
import axios from 'axios';
import * as cookieParser from 'cookie-parser';
import { config } from 'firebase-functions';
import { firestore } from 'firebase-admin';
import { colRefs } from '../../constants/db-refs';
const app = express();
app.use(cors());
app.use(cookieParser());
app.listen();
const { client_key, client_secret } = config().tikTokCredentials;
const redirectURI = `https://xxxxx.firebaseapp.com/linkTikTok/validate`;
app.get('/linkTikTok/oauth', async (req, res) => {
// The user's id;
const uid = 'a_user_id';
if (!uid) {
return res.status(401).send('This action requires user authentication');
}
// Random state
const csrfState = Math.random().toString(36).substring(7);
const state: any = {
state: csrfState,
timestamp: firestore.Timestamp.now(),
uid,
};
// A state object kepts in firestore
await colRefs.tikTokAuthState.doc(uid).set(state);
res.cookie('__session', { state: csrfState });
let url = 'https://open-api.tiktok.com/platform/oauth/connect/';
url += `?client_key=${client_key}`;
url += '&scope=user.info.basic,video.list';
url += '&response_type=code';
url += `&redirect_uri=${redirectURI}`;
url += '&state=' + csrfState;
return res.redirect(url);
});
app.get('/linkTikTok/validate', async (req, res) => {
// Query state
const state = req.query.state as string;
if (!state) {
return res.status(403).send('No state found');
}
const code = req.query.code as string;
if (!code) {
return res.status(403).send('No code found');
}
const sessionCookie = req.cookies['__session'] ?? {};
const sessionState = sessionCookie.state;
if (state !== sessionState) {
return res.status(403).send('Wrong state');
}
// Retrieve the uid from firestore
const uid = await (async () => {
const states = (await colRefs.tikTokAuthState.where('state', '==', state).get()).docs.map(d => d.data());
if (states.length !== 0 && states.length > 1) {
console.warn('More than one state');
}
return states[0].uid;
})();
console.log({ uid });
const URL = `https://open-api.tiktok.com/oauth/access_token`;
const params = {
client_key,
client_secret,
code,
grant_type: 'authorization_code',
};
try {
const result = await axios.post<any>(URL, '', {
params,
});
const data = result.data.data;
const {
access_token: accessToken,
refresh_token,
refresh_expires_in,
open_id: openId,
expires_in,
} = data;
if (!accessToken) {
throw new Error('No access token found');
}
// Application logic
...
});
would you share the piece of code you've written so that we could find the spot.
I got the same error in my code, however, in my case, I was doing duplicate authentication with the TikTok API, because I forgot the "code" GET parameter in my URL and when I was saving settings in my app again, the GET parameter fired again the authentication sequence and I got always the "Authorization code expired" error - but only the second time I was making requests.
You should check if you don't also have duplicate authentication requests in your app.

Why can't Clarifai validate model output request with API key generated in the Clarifai Portal or with the Personal Access token?

Update
I'm able to get my original code, and the suggestions as well working when running it in isolation. However, what I need to do is call it from within a Firebase onRequest or onCall function. When this code gets wrapped by these, the malformed headers and request for authorization are still an issue. We use many other APIs this way so it's puzzling why the Clarifiai API is having these issues. Any suggestions on using it with Firebase?
Original
New to Clarifai and having some authentication issues while attempting to retrieve model outputs from the Food Model.
I've tried two different keys:
API key generated from an app I created in the Portal
API key - the Personal Access Token I generated for myself
In both cases I encounter an Empty or malformed authorization header response.
{
"status":{
"code":11102,
"description":"Invalid request",
"details":"Empty or malformed authorization header. Please provide an API key or session token.",
"req_id":"xyzreasdfasdfasdfasdfasf"
},
"outputs":[
]
}
I've following the following articles to piece together this code. This is running in a Node 10 environment.
Initialization
Food Model
Prediction
const { ClarifaiStub } = require('clarifai-nodejs-grpc');
const grpc = require('#grpc/grpc-js');
const stub = ClarifaiStub.json();
const metadata = new grpc.Metadata();
metadata.set("authorization", "Key xyzKey");
return new Promise((resolve, reject) => {
stub.PostModelOutputs(
{
model_id: 'bd367be194cf45149e75f01d59f77ba7',
inputs: [{ data: { image: { url: 'https://samples.clarifai.com/metro-north.jpg' } } }],
},
metadata,
(err, response) => {
if (err) {
return reject(`ERROR: ${err}`);
}
resolve(JSON.stringify(response));
}
);
});
}
Update: There was an issue in versions prior to 7.0.2 where, if you had another library using #grpc/grpc-js with a different version, the grpc.Metadata object wasn't necessarily constructed from the library version that clarifai-grpc-nodejs was using.
To fix the issue, update the clarifai-grpc-nodejs library, and require the grpc object like this:
const {ClarifaiStub, grpc} = require("clarifai-nodejs-grpc");
Previously, the grpc object was imported directly from #grpc/grpc-js, which was the source of the problem.
There are two ways of authenticating to the Clarifai API:
with an API key, which is application-specific, meaning that an API key is attached to an application and can only do operations inside that application,
with a Personal Access Token (PAT), which is user-specific, which means you can assess / manipulate / do operations on all the applications the user owns / has access to (and also create/update/delete applications themselves).
When using a PAT, you have to specify, in your request data, which application you are targeting. With an API key this is not needed.
I've tested your example (using Node 12, though it should work in 10 as well) with a valid API key and it works fina (after putting it into an async function). Here's a full runnable example (replace YOUR_API_KEY with your valid API key).
function predict() {
const { ClarifaiStub } = require('clarifai-nodejs-grpc');
const grpc = require('#grpc/grpc-js');
const stub = ClarifaiStub.json();
const metadata = new grpc.Metadata();
metadata.set("authorization", "Key YOUR_API_KEY");
return new Promise((resolve, reject) => {
stub.PostModelOutputs(
{
model_id: 'bd367be194cf45149e75f01d59f77ba7',
inputs: [{ data: { image: { url: 'https://samples.clarifai.com/metro-north.jpg' } } }],
},
metadata,
(err, response) => {
if (err) {
return reject(`ERROR: ${err}`);
}
resolve(JSON.stringify(response));
}
);
});
}
async function main() {
const response = await predict();
console.log(response);
}
main();
If you want to use a PAT in the above example, two things must change. Firstly, replace the API key with a PAT:
...
metadata.set("authorization", "Key YOUR_PAT");
...
To the method request object, add the application ID.
...
stub.PostModelOutputs(
{
user_app_id: {
user_id: "me", // The literal "me" resolves to your user ID.
app_id: "YOUR_APPLICATION_ID"
},
model_id: 'bd367be194cf45149e75f01d59f77ba7',
inputs: [{ data: { image: { url: 'https://samples.clarifai.com/metro-north.jpg' } } }],
},
...
Make sure that you have respected the format to pass the key in your code as such:
const metadata = new grpc.Metadata();
metadata.set("authorization", "Key {YOUR_CLARIFAI_API_KEY}");
Make sure that "Key" is present.
Let me know.
EDIT: So looks like Firebase doesn't support custom headers. This is likely impacting the 'Authorization' header. At least this is my best guess. See the comments in the following ticket.
Firebase hosting custom headers not working
The following code works for me:
{
const { ClarifaiStub } = require('clarifai-nodejs-grpc');
const grpc = require('#grpc/grpc-js');
const stub = ClarifaiStub.json();
const metadata = new grpc.Metadata();
metadata.set("authorization", "Key {APP API KEY}");
return new Promise((resolve, reject) => {
stub.PostModelOutputs(
{
model_id: 'bd367be194cf45149e75f01d59f77ba7',
inputs: [{ data: { image: { url: 'https://samples.clarifai.com/metro-north.jpg' } } }],
},
metadata,
(err, response) => {
if (err) {
return reject(`ERROR: ${err}`);
}
console.log(JSON.stringify(response));
resolve(JSON.stringify(response));
}
);
});
}
There was a missing { although I'm not sure if that is what is reflected in the actual code you are running. I'm using in this case an APP API Key (when you create an App, there will be an API Key on the Application Details page.
It sounds like you might be using a Personal Access Token instead, which can be used like this:
{
const { ClarifaiStub } = require('clarifai-nodejs-grpc');
const grpc = require('#grpc/grpc-js');
const stub = ClarifaiStub.json();
const metadata = new grpc.Metadata();
metadata.set("authorization", "Key {Personal Access Token}"); // Sounds like you've made the personal access token correctly - go into settings, then authentication, then create one. Make sure it has proper permissions (I believe all by default).
return new Promise((resolve, reject) => {
stub.PostModelOutputs(
{
user_app_id: {
user_id: "{USER ID}", // I used my actual ID, I did not put 'me'. You can find this under your profile.
app_id: "{APP NAME}" // This is the app ID found in the upper left corner of the app after it is created - not the API Key. This is generally what you named the app when you created it.
},
model_id: 'bd367be194cf45149e75f01d59f77ba7',
inputs: [{ data: { image: { url: 'https://samples.clarifai.com/metro-north.jpg' } } }],
},
metadata,
(err, response) => {
if (err) {
return reject(`ERROR: ${err}`);
}
console.log(JSON.stringify(response));
resolve(JSON.stringify(response));
}
);
});
}
Make sure to fill out the: {Personal Access Token}, {USER ID} and {APP NAME}. I used my actual user id (found in the profile), and the app name is not the API Key for the app, but the name in the upper left corner when you're on the Application details page. This call worked for me.

Working node example of calling google api with jwt for service-to-service call to raw endpoint?

I've been putzing with trying to call the jobs.Insert bigquery rest api endpoint with node (the jobs.Insert method does not seem to be exposed in the bigquery node library).
I've got the Service-to-Service stuff set up so that I can successfully call the methods that the bigquery node library has (create the json file that has the private key, etc. in it for service-to-service calls).
As far as I can tell, I should be able to do call the rest api directly with a signed jwt as the bearer token without having to go through a two-step OAuth process.
I've got stuff to sign a jwt but still getting authentication errors trying to call the raw api just with curl (as a first step) via something like
curl -H "Authorization: Bearer my_signed_jwt" https://www.googleapis.com/bigquery/v2/projects/my_project_id/datasets
("Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential."
Does anyone have an example of doing this? Might be missing just a simple thing that a working example would make obvious.
thanks
You can use this working example which does
Init query object
Init oAuth2 object
Call bigQuery.Jobs.insert
if (!global._babelPolyfill) {
var a = require("babel-polyfill")
}
import {google} from 'googleapis'
let bigQuery = google.bigquery("v2")
describe('Check API', async () => {
it('Test query', async () => {
let result = await test('panada')
})
async function test(p1) {
try {
let query = `SELECT url FROM \`publicdata.samples.github_nested\`
WHERE repository.owner = 'panada'`
let auth = getBasicAuthObj()
auth.setCredentials({
access_token: "myAccessToken",
refresh_token: "myRefreshToken"
})
let request = {
"projectId": "myProject",
auth,
"resource": {
"projectId": "myProject",
"configuration": {
"query": {
query,
"useLegacySql": false
},
"dryRun": false
}
}
}
console.log(`query is: ${query}`)
let result = await callBQ(request) //Check JOB status to make sure it's done
console.log(`result is: ${JSON.stringify(result.data)}`)
result.forEach((row, index) => {
console.log(`row number ${index}, url is: ${row.url}`)
})
} catch (err) {
console.log("err", err)
}
}
/**
* Call BigQuery jobs.insert
* #param request
* #returns {Promise}
*/
async function callBQ(request) {
debugger
console.log("request", request)
try {
let result = await bigQuery.jobs.insert(request, request)//, (err, results) => {
console.log(`All good.....`)
return result
} catch (e) {
console.log(`Failed to run query: ${e}`)
}
}
/**
* Create oAuth object
* #returns {OAuth2Client}
*/
function getBasicAuthObj() {
let clientId = 'myClientId'
let clientSecret = 'mySecret'
let redirectUrl = 'url'
return new google.auth.OAuth2(
clientId,
clientSecret,
redirectUrl
)
}
})
note: You need to add this line to your package.json
"googleapis": "34.0.0"
ok - the trick as to my original question had to do with getting an access token for use in the api call.
const { JWT } = require('google-auth-library');
function getJWTResultWithAccessAndRefreshToken(jsonObjectFromGoogleKeyEtcFile,
callbackWithErrAndResult) {
var scopes = [
"https://www.googleapis.com/auth/bigquery",
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/devstorage.full_control",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/devstorage.read_write"
];
var jwt = new JWT(
jsonObjectFromGoogleKeyEtcFile.client_email,
null,
jsonObjectFromGoogleKeyEtcFile.private_key,
scopes);
jwt.authorize(function (err, result) {
callbackWithErrAndResult(err, result.access_token, result.refresh_token);
});
}
Here, jsonObjectFromGoogleKeyEtcFile is the json object from the json file you get when you generate "Service account keys"/Credentials in the Google Cloud Platform APIs & Services page.
The access_token generated can be used to make a call like below - which worked - where I used the access_token from the function above, and got the projectId from the project_id property of jsonObjectFromGoogleKeyEtcFile:
curl -H "Authorization: Bearer generated_via_jwt_access_token" \
https://www.googleapis.com/bigquery/v2/projects/projectId/datasets
Interestingly, you get a refresh_token, too, but it has value "jwt-placeholder"
Whew.

Resources