Skip to content

Commit 8672516

Browse files
committed
twilio, and using new envvars
1 parent 9ef3514 commit 8672516

5 files changed

Lines changed: 242 additions & 1 deletion

File tree

functions/crypto-login/__tests__/index.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,17 @@ describe('Crypto Login Function (Integration)', () => {
4949
image: 'constructive/function-test-runner:v2',
5050
imagePullPolicy: "IfNotPresent",
5151
command: ["npx", "ts-node", "functions/_runtimes/node/runner.js", "functions/crypto-login/src/index.ts"],
52-
env: [{ name: "PORT", value: "8080" }]
52+
env: [
53+
{ name: "PORT", value: "8080" },
54+
{ name: "PGHOST", value: "postgres" },
55+
{ name: "PGPASSWORD", value: process.env.PGPASSWORD },
56+
{ name: "STRIPE_PUBLISHABLE_KEY", value: process.env.STRIPE_PUBLISHABLE_KEY },
57+
{ name: "STRIPE_SECRET_KEY", value: process.env.STRIPE_SECRET_KEY },
58+
{ name: "TWILIO_ACCOUNT_SID", value: process.env.TWILIO_ACCOUNT_SID },
59+
{ name: "TWILIO_AUTH_TOKEN", value: process.env.TWILIO_AUTH_TOKEN },
60+
{ name: "CALVIN_API_KEY", value: process.env.CALVIN_API_KEY },
61+
{ name: "OPENAI_API_KEY", value: process.env.OPENAI_API_KEY }
62+
]
5363
}]
5464
}
5565
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
2+
import { getConnections, PgTestClient } from 'pgsql-test';
3+
import { KubernetesClient } from 'kubernetesjs';
4+
5+
describe('Twilio SMS Function (Integration)', () => {
6+
let db: PgTestClient;
7+
let pg: PgTestClient;
8+
let teardown: () => Promise<void>;
9+
let k8s: KubernetesClient;
10+
const NAMESPACE = 'default';
11+
let proxyProcess: any;
12+
13+
beforeAll(async () => {
14+
const { spawn } = require('child_process');
15+
proxyProcess = spawn('kubectl', ['proxy', '--port=8009']);
16+
await new Promise(resolve => setTimeout(resolve, 2000));
17+
k8s = new KubernetesClient({ restEndpoint: 'http://127.0.0.1:8009' } as any);
18+
19+
// database connection in the pod
20+
({ pg, db, teardown } = await getConnections({
21+
pg: {
22+
user: 'postgres',
23+
password: process.env.PGPASSWORD,
24+
host: process.env.PGHOST,
25+
port: Number(process.env.PGPORT || 5432),
26+
database: String(process.env.PGDATABASE || `twilio_sms_test_${Math.floor(Math.random() * 100000)}`)
27+
},
28+
db: {
29+
connections: { app: { user: 'postgres', password: process.env.PGPASSWORD } }
30+
}
31+
}));
32+
});
33+
34+
afterAll(async () => {
35+
await teardown();
36+
if (proxyProcess) proxyProcess.kill();
37+
});
38+
39+
it('should verify database connectivity via pgsql-test', async () => {
40+
const result = await pg.query('SELECT 1 as num');
41+
expect(result.rows[0].num).toBe(1);
42+
});
43+
44+
it('should orchestrate the twilio-sms job and verify startup', async () => {
45+
const jobName = `twilio-sms-exec-${Math.floor(Date.now() / 1000)}`;
46+
try { await k8s.deleteBatchV1NamespacedJob({ path: { namespace: NAMESPACE, name: jobName }, query: { propagationPolicy: 'Background' } }); } catch (e) { }
47+
48+
const jobManifest = {
49+
apiVersion: 'batch/v1',
50+
kind: 'Job',
51+
metadata: { name: jobName, namespace: NAMESPACE, labels: { "job-name": jobName, "app": "twilio-sms" } },
52+
spec: {
53+
backoffLimit: 0,
54+
template: {
55+
metadata: { labels: { "job-name": jobName } },
56+
spec: {
57+
restartPolicy: 'Never',
58+
containers: [{
59+
name: 'twilio-sms',
60+
image: 'constructive/function-test-runner:v2',
61+
imagePullPolicy: "IfNotPresent",
62+
command: ["npx", "ts-node", "functions/_runtimes/node/runner.js", "functions/twilio-sms/src/index.ts"],
63+
env: [
64+
{ name: "PORT", value: "8080" },
65+
// Propagate env vars from the test runner pod to the function pod
66+
{ name: "TWILIO_ACCOUNT_SID", value: process.env.TWILIO_ACCOUNT_SID },
67+
{ name: "TWILIO_AUTH_TOKEN", value: process.env.TWILIO_AUTH_TOKEN },
68+
{ name: "TWILIO_FROM_NUMBER", value: process.env.TWILIO_FROM_NUMBER },
69+
{ name: "PGHOST", value: "postgres" },
70+
{ name: "PGPASSWORD", value: process.env.PGPASSWORD }
71+
]
72+
}]
73+
}
74+
}
75+
}
76+
};
77+
78+
await k8s.createBatchV1NamespacedJob({ path: { namespace: NAMESPACE }, body: jobManifest, query: {} });
79+
80+
let success = false;
81+
let logsResponse = '';
82+
let podName = '';
83+
84+
for (let i = 0; i < 30; i++) {
85+
try {
86+
if (!podName) {
87+
const pods = await k8s.listCoreV1NamespacedPod({ path: { namespace: NAMESPACE }, query: { labelSelector: `job-name=${jobName}` } });
88+
if (pods.items && pods.items.length > 0) podName = pods.items[0].metadata.name;
89+
}
90+
if (podName) {
91+
try {
92+
const res = await fetch(`http://127.0.0.1:8009/api/v1/namespaces/${NAMESPACE}/pods/${podName}/log?tailLines=50`);
93+
const logs = await res.text();
94+
if (logs.includes('listening on port')) {
95+
success = true;
96+
logsResponse = logs;
97+
break;
98+
}
99+
logsResponse = logs;
100+
} catch (e) { }
101+
}
102+
} catch (e) { }
103+
await new Promise(r => setTimeout(r, 2000));
104+
}
105+
106+
if (!success) throw new Error(`Twilio SMS Service Failed: ${logsResponse}`);
107+
expect(success).toBe(true);
108+
109+
try { await k8s.deleteBatchV1NamespacedJob({ path: { namespace: NAMESPACE, name: jobName }, query: { propagationPolicy: 'Background' } }); } catch (e) { }
110+
}, 120000);
111+
});

functions/twilio-sms/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@constructive-io/twilio-sms-fn",
3+
"version": "0.1.0",
4+
"description": "Twilio SMS Cloud Function",
5+
"private": false,
6+
"publishConfig": {
7+
"access": "public",
8+
"directory": "dist"
9+
},
10+
"main": "dist/index.js",
11+
"types": "dist/index.d.ts",
12+
"files": [
13+
"dist"
14+
],
15+
"scripts": {
16+
"build": "tsc -p tsconfig.json",
17+
"clean": "rimraf dist",
18+
"pretest": "tsc",
19+
"test": "jest --forceExit __tests__/index.test.ts",
20+
"start": "node ../../_runtimes/node/runner.js dist/index.js"
21+
},
22+
"dependencies": {
23+
"@constructive-io/knative-job-fn": "latest",
24+
"twilio": "^4.23.0",
25+
"graphql-tag": "^2.12.6",
26+
"cross-fetch": "^4.0.0",
27+
"graphql-request": "^6.1.0"
28+
},
29+
"devDependencies": {
30+
"pgsql-test": "latest",
31+
"@types/node": "^22.10.4",
32+
"@types/jest": "^29.5.12",
33+
"jest": "^29.7.0",
34+
"ts-jest": "^29.1.2",
35+
"typescript": "^5.1.6"
36+
}
37+
}

functions/twilio-sms/src/index.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Twilio } from 'twilio';
2+
import gql from 'graphql-tag';
3+
4+
const GetUsers = gql`
5+
query GetUsers {
6+
users {
7+
nodes {
8+
id
9+
username
10+
}
11+
}
12+
}
13+
`;
14+
15+
export default async (params: any, context: any) => {
16+
const { client } = context;
17+
console.log('[twilio-sms] Request received', params);
18+
const { to, body } = params || {};
19+
20+
// Verify GQL connection
21+
try {
22+
const data = await client.request(GetUsers);
23+
console.log('[twilio-sms] GQL Check Success:', data?.users?.nodes?.length ? 'Users found' : 'No users');
24+
} catch (e: any) {
25+
console.warn('[twilio-sms] GQL Request failed:', e.message);
26+
}
27+
28+
29+
// Env vars should be injected by the runtime/k8s
30+
const accountSid = process.env.TWILIO_ACCOUNT_SID;
31+
const authToken = process.env.TWILIO_AUTH_TOKEN;
32+
const fromNumber = process.env.TWILIO_FROM_NUMBER;
33+
34+
if (!accountSid || !authToken || !fromNumber) {
35+
console.error("Missing Twilio configuration");
36+
return { error: "Missing Twilio configuration (SID, TOKEN, FROM)" };
37+
}
38+
39+
if (!to || !body) {
40+
return { error: "Missing 'to' or 'body' in request" };
41+
}
42+
43+
try {
44+
const clientFn = new Twilio(accountSid, authToken);
45+
46+
console.log(`Sending SMS to ${to}: ${body}`);
47+
48+
// In test mode, we might want to mock this or use test credentials.
49+
const message = await clientFn.messages.create({
50+
body: body,
51+
from: fromNumber,
52+
to: to
53+
});
54+
55+
console.log(`SMS sent: ${message.sid}`);
56+
return { success: true, sid: message.sid, status: message.status };
57+
58+
} catch (e: any) {
59+
console.error('[twilio-sms] Error sending SMS:', e);
60+
throw e; // specific error handling can remain or bubble up
61+
}
62+
};

functions/twilio-sms/tsconfig.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "commonjs",
5+
"rootDir": "src",
6+
"outDir": "dist",
7+
"strict": true,
8+
"esModuleInterop": true,
9+
"skipLibCheck": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"declaration": true
12+
},
13+
"include": [
14+
"src/**/*"
15+
],
16+
"exclude": [
17+
"node_modules",
18+
"__tests__",
19+
"dist"
20+
]
21+
}

0 commit comments

Comments
 (0)