Skip to content

Commit 4555400

Browse files
committed
Merge branch 'release-25.3' into 'main'
Release merge for 25.3 See merge request nwac/sdk-ts!90
2 parents f2a2213 + c4e0d8c commit 4555400

File tree

10 files changed

+768
-11
lines changed

10 files changed

+768
-11
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { NetworkAsCodeClient } from "../src";
2+
import { beforeAll, describe, expect} from "@jest/globals";
3+
import "dotenv/config";
4+
import { Device } from "../src/models/device";
5+
import { configureClient } from "./configClient";
6+
7+
let client: NetworkAsCodeClient;
8+
let device: Device;
9+
10+
beforeAll(() => {
11+
client = configureClient();
12+
device = client.devices.get({
13+
phoneNumber:"+3637123456",
14+
});
15+
});
16+
17+
describe("Geofencing", () => {
18+
it("should subscribe for geofencing event area entered", async () => {
19+
const subscription = await client.geofencing.subscribe(device, {
20+
sink: "https://example.com/notif",
21+
types: ["org.camaraproject.geofencing-subscriptions.v0.area-entered"],
22+
latitude: 47.48627616952785,
23+
longitude: 19.07915612501993,
24+
radius: 2000
25+
});
26+
27+
expect(subscription.eventSubscriptionId).toBeTruthy();
28+
29+
subscription.delete();
30+
});
31+
32+
it("should subscribe for geofencing event area left", async () => {
33+
const subscription = await client.geofencing.subscribe(device, {
34+
sink: "https://example.com/notif",
35+
types: ["org.camaraproject.geofencing-subscriptions.v0.area-left"],
36+
latitude: 47.48627616952785,
37+
longitude: 19.07915612501993,
38+
radius: 2000
39+
});
40+
41+
expect(subscription.eventSubscriptionId).toBeTruthy();
42+
43+
subscription.delete();
44+
});
45+
46+
it("should subscribe for geofencing event with plain credential", async () => {
47+
const subscription = await client.geofencing.subscribe(device, {
48+
sink: "https://example.com/notif",
49+
types: ["org.camaraproject.geofencing-subscriptions.v0.area-left"],
50+
latitude: 47.48627616952785,
51+
longitude: 19.07915612501993,
52+
radius: 2000,
53+
sinkCredential: {
54+
credentialType:"PLAIN",
55+
identifier: "client-id",
56+
secret: "client-secret"
57+
}
58+
});
59+
60+
expect(subscription.eventSubscriptionId).toBeTruthy();
61+
62+
subscription.delete();
63+
});
64+
65+
it("should subscribe for geofencing event with accesstoken credential", async () => {
66+
const expirationDate = new Date(Date.now() + 5 * 60 * 60 * 1000);
67+
expirationDate.setMilliseconds(0);
68+
const subscription = await client.geofencing.subscribe(device, {
69+
sink: "https://example.com/notif",
70+
types: ["org.camaraproject.geofencing-subscriptions.v0.area-left"],
71+
latitude: 47.48627616952785,
72+
longitude: 19.07915612501993,
73+
radius: 2000,
74+
sinkCredential: {
75+
credentialType:"ACCESSTOKEN",
76+
accessToken: "some-access-token",
77+
accessTokenType: "bearer",
78+
accessTokenExpiresUtc: expirationDate
79+
}
80+
});
81+
82+
expect(subscription.eventSubscriptionId).toBeTruthy();
83+
84+
subscription.delete();
85+
});
86+
87+
it("should get an event subscription", async () => {
88+
const subscription = await client.geofencing.subscribe(device, {
89+
sink: "https://example.com/notif",
90+
types: ["org.camaraproject.geofencing-subscriptions.v0.area-entered"],
91+
latitude: 47.48627616952785,
92+
longitude: 19.07915612501993,
93+
radius: 2000
94+
});
95+
96+
expect(subscription.eventSubscriptionId).toBeTruthy();
97+
98+
const fetchedSubscription = await client.geofencing.get(subscription.eventSubscriptionId as string);
99+
100+
expect(fetchedSubscription.eventSubscriptionId).toBeTruthy();
101+
102+
subscription.delete();
103+
});
104+
105+
it("should get all event subscriptions", async () => {
106+
const subscription = await client.geofencing.subscribe(device, {
107+
sink: "https://example.com/notif",
108+
types: ["org.camaraproject.geofencing-subscriptions.v0.area-entered"],
109+
latitude: 47.48627616952785,
110+
longitude: 19.07915612501993,
111+
radius: 2000
112+
});
113+
114+
expect(subscription.eventSubscriptionId).toBeTruthy();
115+
116+
const fetchedSubscriptions = await client.geofencing.getAll();
117+
118+
expect(fetchedSubscriptions.length).toBeGreaterThanOrEqual(0);
119+
120+
subscription.delete();
121+
});
122+
123+
it("should delete an event subscription", async () => {
124+
const subscription = await client.geofencing.subscribe(device, {
125+
sink: "https://example.com/notif",
126+
types: ["org.camaraproject.geofencing-subscriptions.v0.area-entered"],
127+
latitude: 47.48627616952785,
128+
longitude: 19.07915612501993,
129+
radius: 2000
130+
});
131+
132+
expect(subscription.eventSubscriptionId).toBeTruthy();
133+
134+
subscription.delete();
135+
136+
try {
137+
await client.geofencing.get(subscription.eventSubscriptionId);
138+
expect(true).toBe(false);
139+
} catch (error){
140+
expect(error).toBeDefined();
141+
}
142+
});
143+
});

jest.config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,10 @@ const config: Config = {
161161
// ],
162162

163163
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
164-
// testPathIgnorePatterns: [
165-
// "/node_modules/"
166-
// ],
164+
testPathIgnorePatterns: [
165+
"tests/geofencing.test.ts",
166+
"integration-tests/geofencing.itest.ts"
167+
],
167168

168169
// The regexp pattern or array of patterns that Jest uses to detect test files
169170
// testRegex: [],

src/api/client.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { DeviceStatusAPI } from "./deviceStatusAPI";
2222
import { AttachAPI, SliceAPI } from "./sliceApi";
2323
import { CongestionInsightsAPI } from "./congestionInsightsApi";
2424
import { SimSwapAPI } from "./simSwapApi";
25+
import { GeofencingAPI } from "./geofencing";
2526

2627
const QOS_BASE_URL_PROD =
2728
"https://quality-of-service-on-demand.p-eu.rapidapi.com";
@@ -58,6 +59,9 @@ const SIM_SWAP_BASE_URL_PROD =
5859
const SIM_SWAP_BASE_URL_DEV =
5960
"https://simswap.p-eu.rapidapi.com/sim-swap/sim-swap/v0";
6061

62+
const GEOFENCING_BASE_URL_PROD = "https://geofencing-subscription.p-eu.rapidapi.com/v0.3"
63+
const GEOFENCING_BASE_URL_DEV = "https://geofencing-subscription.p-eu.rapidapi.com/v0.3"
64+
6165
const agent = new ProxyAgent();
6266

6367
export class APIClient {
@@ -69,6 +73,7 @@ export class APIClient {
6973
sliceAttach: AttachAPI;
7074
insights: CongestionInsightsAPI;
7175
simSwap: SimSwapAPI;
76+
geofencing: GeofencingAPI;
7277

7378
constructor(
7479
token: string,
@@ -81,6 +86,7 @@ export class APIClient {
8186
sliceAttachBaseUrl: string = SLICE_ATTACH_BASE_URL_PROD,
8287
congestionInsightsBaseUrl: string = CONGESTION_INSIGHTS_BASE_URL_PROD,
8388
simSwapBaseUrl: string = SIM_SWAP_BASE_URL_PROD,
89+
geofencingBaseUrl: string = GEOFENCING_BASE_URL_PROD,
8490
) {
8591
if (devMode && qosBaseUrl == QOS_BASE_URL_PROD) {
8692
qosBaseUrl = QOS_BASE_URL_DEV;
@@ -218,5 +224,22 @@ export class APIClient {
218224
.replace("p-eu", "nokia"),
219225
agent
220226
);
227+
228+
if (devMode && geofencingBaseUrl == GEOFENCING_BASE_URL_PROD) {
229+
geofencingBaseUrl = GEOFENCING_BASE_URL_DEV;
230+
}
231+
232+
this.geofencing = new GeofencingAPI(
233+
geofencingBaseUrl,
234+
token,
235+
devMode
236+
? geofencingBaseUrl
237+
.replace("https://", "")
238+
.replace("p-eu", "nokia-dev")
239+
: geofencingBaseUrl
240+
.replace("https://", "")
241+
.replace("p-eu", "nokia"),
242+
agent
243+
);
221244
}
222245
}

src/api/geofencing.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright 2025 Nokia
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ProxyAgent } from "proxy-agent";
18+
import fetch from "node-fetch";
19+
20+
import { errorHandler } from "../errors";
21+
import { Device } from "../models/device";
22+
import { GeofencingSubscriptionParams } from "../models/geofencing";
23+
24+
export class GeofencingAPI {
25+
private baseUrl: string;
26+
private headers: HeadersInit;
27+
private agent: ProxyAgent;
28+
29+
constructor(
30+
baseUrl: string,
31+
rapidKey: string,
32+
rapidHost: string,
33+
agent: ProxyAgent
34+
) {
35+
this.baseUrl = baseUrl;
36+
this.headers = {
37+
"Content-Type": "application/json",
38+
"X-RapidAPI-Host": rapidHost,
39+
"X-RapidAPI-Key": rapidKey,
40+
};
41+
this.agent = agent;
42+
}
43+
44+
async subscribe(
45+
device: Device,
46+
params: GeofencingSubscriptionParams
47+
): Promise<any> {
48+
const body: any = {
49+
protocol: "HTTP",
50+
sink: params.sink,
51+
types: params.types,
52+
config: {
53+
subscriptionDetail: {
54+
device: device.toJson(),
55+
area: {
56+
areaType: "CIRCLE",
57+
center: {
58+
latitude: params.latitude,
59+
longitude: params.longitude
60+
},
61+
radius: params.radius
62+
}
63+
}
64+
}
65+
};
66+
67+
if (params.sinkCredential) {
68+
body.sinkCredential = params.sinkCredential ? Object.fromEntries(Object.entries(params.sinkCredential as {[key:string]: any}).filter(([, value]) => value !== null && value !== undefined)) : undefined;
69+
}
70+
71+
if (params.subscriptionExpireTime) {
72+
body.config.subscriptionExpireTime = params.subscriptionExpireTime;
73+
}
74+
75+
if (params.subscriptionMaxEvents) {
76+
body.config.subscriptionMaxEvents = params.subscriptionMaxEvents;
77+
}
78+
79+
if (params.initialEvent) {
80+
body.config.initialEvent = params.initialEvent;
81+
}
82+
83+
const response = await fetch(`${this.baseUrl}/subscriptions`, {
84+
method: "POST",
85+
headers: this.headers,
86+
body: JSON.stringify(body),
87+
agent: this.agent,
88+
});
89+
90+
errorHandler(response);
91+
92+
return response.json() as Promise<any>;
93+
}
94+
95+
async delete(eventSubscriptionId: string) {
96+
const response = await fetch(
97+
`${this.baseUrl}/subscriptions/${eventSubscriptionId}`,
98+
{
99+
method: "DELETE",
100+
headers: this.headers,
101+
agent: this.agent,
102+
}
103+
);
104+
105+
errorHandler(response);
106+
}
107+
108+
async get(eventSubscriptionId: string) {
109+
const response = await fetch(
110+
`${this.baseUrl}/subscriptions/${eventSubscriptionId}`,
111+
{
112+
method: "GET",
113+
headers: this.headers,
114+
agent: this.agent,
115+
}
116+
);
117+
118+
errorHandler(response);
119+
120+
return response.json() as Promise<any>;
121+
}
122+
123+
async getSubscriptions() {
124+
const response = await fetch(`${this.baseUrl}/subscriptions`, {
125+
method: "GET",
126+
headers: this.headers,
127+
agent: this.agent,
128+
});
129+
130+
errorHandler(response);
131+
132+
return response.json() as Promise<any>;
133+
}
134+
}

src/api/locationApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async verifyLocation(
5858
maxAge,
5959
};
6060

61-
const response = await fetch(`${this.baseUrl}/verify`, {
61+
const response = await fetch(`${this.baseUrl}/v1/verify`, {
6262
method: "POST",
6363
headers: this.headers,
6464
body: JSON.stringify(body),

src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { APIClient } from "./api/client";
1818
import { CongestionInsights } from "./namespaces/congestionInsights";
1919
import { Devices } from "./namespaces/device";
2020
import { DeviceStatus } from "./namespaces/deviceStatus";
21+
import { Geofencing } from "./namespaces/geofencing";
2122
import { Sessions } from "./namespaces/session";
2223
import { Slices } from "./namespaces/slice";
2324

@@ -40,6 +41,7 @@ export class NetworkAsCodeClient {
4041
private _deviceStatus: DeviceStatus;
4142
private _slices: Slices;
4243
private _insights: CongestionInsights;
44+
private _geofencing: Geofencing;
4345

4446
constructor(token: string, devMode?: boolean) {
4547
this._api = new APIClient(token, devMode);
@@ -48,6 +50,7 @@ export class NetworkAsCodeClient {
4850
this._deviceStatus = new DeviceStatus(this._api);
4951
this._slices = new Slices(this._api);
5052
this._insights = new CongestionInsights(this._api);
53+
this._geofencing = new Geofencing(this._api);
5154
}
5255

5356
/**
@@ -90,6 +93,14 @@ export class NetworkAsCodeClient {
9093
return this._insights;
9194
}
9295

96+
/**
97+
* Namespace containing functionalities related to geofencing.
98+
* @returns NAC geofencing
99+
*/
100+
// get geofencing() {
101+
// return this._geofencing;
102+
// }
103+
93104
/**
94105
* @returns NAC API
95106
*/

0 commit comments

Comments
 (0)