Skip to content

Commit 5274fd5

Browse files
authored
feat: add fastly:device module which allows applications to detect a device based on a user-agent (#738)
1 parent 9892d90 commit 5274fd5

21 files changed

+1189
-1
lines changed

integration-tests/js-compute/fixtures/app/fastly.toml.in

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ service_id = ""
101101
key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
102102
data = "This is some secret data"
103103

104+
[local_server.device_detection]
105+
format = "inline-toml"
106+
107+
[local_server.device_detection.user_agents]
108+
[local_server.device_detection.user_agents."Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5"]
109+
user_agent = {}
110+
os = {}
111+
device = {name = "iPhone", brand = "Apple", model = "iPhone4,1", hwtype = "Mobile Phone", is_ereader = false, is_gameconsole = false, is_mediaplayer = false, is_mobile = true, is_smarttv = false, is_tablet = false, is_tvplayer = false, is_desktop = false, is_touchscreen = true }
112+
113+
[local_server.device_detection.user_agents."ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)"]
114+
user_agent = {}
115+
os = {}
116+
device = {name = "Asus TeK", brand = "Asus", model = "TeK", is_desktop = false }
117+
104118

105119
[setup]
106120
[setup.backends]

integration-tests/js-compute/fixtures/app/src/assertions.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ export function deepEqual(a, b) {
123123
}
124124
return false;
125125
}
126+
126127
// Case: `a` is of type 'object'
127-
if (typeB !== 'object') {
128+
if (b === null || typeB !== 'object') {
128129
return false;
129130
}
130131
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/// <reference path="../../../../../types/index.d.ts" />
2+
/* eslint-env serviceworker */
3+
4+
import { pass, assert, assertThrows } from "./assertions.js";
5+
import { Device } from 'fastly:device';
6+
import { routes } from "./routes.js";
7+
8+
let error;
9+
routes.set("/device/interface", () => {
10+
let actual = Reflect.ownKeys(Device)
11+
let expected = ["prototype", "lookup", "length", "name"]
12+
error = assert(actual, expected, `Reflect.ownKeys(Device)`)
13+
if (error) { return error }
14+
15+
// Check the prototype descriptors are correct
16+
{
17+
actual = Reflect.getOwnPropertyDescriptor(Device, 'prototype')
18+
expected = {
19+
"value": Device.prototype,
20+
"writable": false,
21+
"enumerable": false,
22+
"configurable": false
23+
}
24+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device, 'prototype')`)
25+
if (error) { return error }
26+
}
27+
28+
// Check the constructor function's defined parameter length is correct
29+
{
30+
actual = Reflect.getOwnPropertyDescriptor(Device, 'length')
31+
expected = {
32+
"value": 0,
33+
"writable": false,
34+
"enumerable": false,
35+
"configurable": true
36+
}
37+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device, 'length')`)
38+
if (error) { return error }
39+
}
40+
41+
// Check the constructor function's name is correct
42+
{
43+
actual = Reflect.getOwnPropertyDescriptor(Device, 'name')
44+
expected = {
45+
"value": "Device",
46+
"writable": false,
47+
"enumerable": false,
48+
"configurable": true
49+
}
50+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device, 'name')`)
51+
if (error) { return error }
52+
}
53+
54+
// Check the prototype has the correct keys
55+
{
56+
actual = Reflect.ownKeys(Device.prototype)
57+
expected = ["constructor", "name", "brand", "model", "hardwareType", "isDesktop", "isGameConsole", "isMediaPlayer", "isMobile", "isSmartTV", "isTablet", "isTouchscreen", "toJSON", Symbol.toStringTag]
58+
error = assert(actual, expected, `Reflect.ownKeys(Device.prototype)`)
59+
if (error) { return error }
60+
}
61+
62+
// Check the constructor on the prototype is correct
63+
{
64+
actual = Reflect.getOwnPropertyDescriptor(Device.prototype, 'constructor')
65+
expected = { "writable": true, "enumerable": false, "configurable": true, value: Device.prototype.constructor }
66+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device.prototype, 'constructor')`)
67+
if (error) { return error }
68+
69+
error = assert(typeof Device.prototype.constructor, 'function', `typeof Device.prototype.constructor`)
70+
if (error) { return error }
71+
72+
actual = Reflect.getOwnPropertyDescriptor(Device.prototype.constructor, 'length')
73+
expected = {
74+
"value": 0,
75+
"writable": false,
76+
"enumerable": false,
77+
"configurable": true
78+
}
79+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device.prototype.constructor, 'length')`)
80+
if (error) { return error }
81+
82+
actual = Reflect.getOwnPropertyDescriptor(Device.prototype.constructor, 'name')
83+
expected = {
84+
"value": "Device",
85+
"writable": false,
86+
"enumerable": false,
87+
"configurable": true
88+
}
89+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device.prototype.constructor, 'name')`)
90+
if (error) { return error }
91+
}
92+
93+
// Check the Symbol.toStringTag on the prototype is correct
94+
{
95+
actual = Reflect.getOwnPropertyDescriptor(Device.prototype, Symbol.toStringTag)
96+
expected = { "writable": false, "enumerable": false, "configurable": true, value: "Device" }
97+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device.prototype, [Symbol.toStringTag])`)
98+
if (error) { return error }
99+
100+
error = assert(typeof Device.prototype[Symbol.toStringTag], 'string', `typeof Device.prototype[Symbol.toStringTag]`)
101+
if (error) { return error }
102+
}
103+
104+
// Check the lookup static method has correct descriptors, length and name
105+
{
106+
actual = Reflect.getOwnPropertyDescriptor(Device, 'lookup')
107+
expected = { "writable": true, "enumerable": true, "configurable": true, value: Device.lookup }
108+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device, 'lookup')`)
109+
if (error) { return error }
110+
111+
error = assert(typeof Device.lookup, 'function', `typeof Device.lookup`)
112+
if (error) { return error }
113+
114+
actual = Reflect.getOwnPropertyDescriptor(Device.lookup, 'length')
115+
expected = {
116+
"value": 1,
117+
"writable": false,
118+
"enumerable": false,
119+
"configurable": true
120+
}
121+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device.lookup, 'length')`)
122+
if (error) { return error }
123+
124+
actual = Reflect.getOwnPropertyDescriptor(Device.lookup, 'name')
125+
expected = {
126+
"value": "lookup",
127+
"writable": false,
128+
"enumerable": false,
129+
"configurable": true
130+
}
131+
error = assert(actual, expected, `Reflect.getOwnPropertyDescriptor(Device.lookup, 'name')`)
132+
if (error) { return error }
133+
}
134+
135+
for (let property of ["name", "brand", "model", "hardwareType", "isDesktop", "isGameConsole", "isMediaPlayer", "isMobile", "isSmartTV", "isTablet", "isTouchscreen"]) {
136+
const descriptors = Reflect.getOwnPropertyDescriptor(Device.prototype, property)
137+
expected = { "enumerable": true, "configurable": true }
138+
error = assert(descriptors.enumerable, true, `Reflect.getOwnPropertyDescriptor(Device, '${property}').enumerable`)
139+
error = assert(descriptors.configurable, true, `Reflect.getOwnPropertyDescriptor(Device, '${property}').configurable`)
140+
error = assert(descriptors.value, undefined, `Reflect.getOwnPropertyDescriptor(Device, '${property}').value`)
141+
error = assert(descriptors.set, undefined, `Reflect.getOwnPropertyDescriptor(Device, '${property}').set`)
142+
error = assert(typeof descriptors.get, 'function', `typeof Reflect.getOwnPropertyDescriptor(Device, '${property}').get`)
143+
if (error) { return error }
144+
}
145+
146+
return pass('ok')
147+
});
148+
149+
// Device constructor
150+
{
151+
152+
routes.set("/device/constructor/called-as-regular-function", () => {
153+
error = assertThrows(() => {
154+
Device()
155+
}, TypeError, `Illegal constructor`)
156+
if (error) { return error }
157+
return pass('ok')
158+
});
159+
routes.set("/device/constructor/throws", () => {
160+
error = assertThrows(() => new Device(), TypeError, `Illegal constructor`)
161+
if (error) { return error }
162+
return pass('ok')
163+
});
164+
}
165+
166+
// Device lookup static method
167+
// static lookup(useragent: string): DeviceEntry | null;
168+
{
169+
routes.set("/device/lookup/called-as-constructor", () => {
170+
let error = assertThrows(() => {
171+
new Device.lookup('1')
172+
}, TypeError, `Device.lookup is not a constructor`)
173+
if (error) { return error }
174+
return pass('ok')
175+
});
176+
// https://tc39.es/ecma262/#sec-tostring
177+
routes.set("/device/lookup/useragent-parameter-calls-7.1.17-ToString", () => {
178+
let sentinel;
179+
const test = () => {
180+
sentinel = Symbol('sentinel');
181+
const useragent = {
182+
toString() {
183+
throw sentinel;
184+
}
185+
}
186+
Device.lookup(useragent)
187+
}
188+
let error = assertThrows(test)
189+
if (error) { return error }
190+
try {
191+
test()
192+
} catch (thrownError) {
193+
let error = assert(thrownError, sentinel, 'thrownError === sentinel')
194+
if (error) { return error }
195+
}
196+
error = assertThrows(() => {
197+
Device.lookup(Symbol())
198+
}, TypeError, `can't convert symbol to string`)
199+
if (error) { return error }
200+
return pass('ok')
201+
});
202+
routes.set("/device/lookup/useragent-parameter-not-supplied", () => {
203+
let error = assertThrows(() => {
204+
Device.lookup()
205+
}, TypeError, `Device.lookup: At least 1 argument required, but only 0 passed`)
206+
if (error) { return error }
207+
return pass('ok')
208+
});
209+
routes.set("/device/lookup/useragent-parameter-empty-string", () => {
210+
let error = assertThrows(() => {
211+
Device.lookup('')
212+
}, Error, `Device.lookup: useragent parameter can not be an empty string`)
213+
if (error) { return error }
214+
return pass('ok')
215+
});
216+
routes.set("/device/lookup/useragent-does-not-exist-returns-null", () => {
217+
let result = Device.lookup(Math.random())
218+
error = assert(result, null, `Device.lookup(Math.random()) === null`)
219+
if (error) { return error }
220+
return pass('ok')
221+
});
222+
routes.set("/device/lookup/useragent-exists-all-fields-identified", () => {
223+
let useragent = "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5";
224+
let device = Device.lookup(useragent);
225+
226+
error = assert(device instanceof Device, true, `Device.lookup(useragent) instanceof DeviceEntry`)
227+
if (error) { return error }
228+
229+
error = assert(device.name, "iPhone", `device.name`)
230+
if (error) { return error }
231+
232+
error = assert(device.brand, "Apple", `device.brand`)
233+
if (error) { return error }
234+
235+
error = assert(device.model, "iPhone4,1", `device.model`)
236+
if (error) { return error }
237+
238+
error = assert(device.hardwareType, "Mobile Phone", `device.hardwareType`)
239+
if (error) { return error }
240+
241+
error = assert(device.isDesktop, false, `device.isDesktop`)
242+
if (error) { return error }
243+
244+
error = assert(device.isGameConsole, false, `device.isGameConsole`)
245+
if (error) { return error }
246+
247+
error = assert(device.isMediaPlayer, false, `device.isMediaPlayer`)
248+
if (error) { return error }
249+
250+
error = assert(device.isMobile, true, `device.isMobile`)
251+
if (error) { return error }
252+
253+
error = assert(device.isSmartTV, false, `device.isSmartTV`)
254+
if (error) { return error }
255+
256+
error = assert(device.isTablet, false, `device.isTablet`)
257+
if (error) { return error }
258+
259+
error = assert(device.isTouchscreen, true, `device.isTouchscreen`)
260+
if (error) { return error }
261+
262+
return pass('ok')
263+
});
264+
routes.set("/device/lookup/useragent-exists-some-fields-identified", () => {
265+
let useragent = "ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)";
266+
let device = Device.lookup(useragent);
267+
268+
error = assert(device instanceof Device, true, `Device.lookup(useragent) instanceof DeviceEntry`)
269+
if (error) { return error }
270+
271+
error = assert(device.name, "Asus TeK", `device.name`)
272+
if (error) { return error }
273+
274+
error = assert(device.brand, "Asus", `device.brand`)
275+
if (error) { return error }
276+
277+
error = assert(device.model, "TeK", `device.model`)
278+
if (error) { return error }
279+
280+
error = assert(device.hardwareType, null, `device.hardwareType`)
281+
if (error) { return error }
282+
283+
error = assert(device.isDesktop, false, `device.isDesktop`)
284+
if (error) { return error }
285+
286+
error = assert(device.isGameConsole, null, `device.isGameConsole`)
287+
if (error) { return error }
288+
289+
error = assert(device.isMediaPlayer, null, `device.isMediaPlayer`)
290+
if (error) { return error }
291+
292+
error = assert(device.isMobile, null, `device.isMobile`)
293+
if (error) { return error }
294+
295+
error = assert(device.isSmartTV, null, `device.isSmartTV`)
296+
if (error) { return error }
297+
298+
error = assert(device.isTablet, null, `device.isTablet`)
299+
if (error) { return error }
300+
301+
error = assert(device.isTouchscreen, null, `device.isTouchscreen`)
302+
if (error) { return error }
303+
304+
error = assert(JSON.stringify(device), "{\"name\":\"Asus TeK\",\"brand\":\"Asus\",\"model\":\"TeK\",\"hardwareType\":null,\"isDesktop\":null,\"isGameConsole\":null,\"isMediaPlayer\":null,\"isMobile\":null,\"isSmartTV\":null,\"isTablet\":null,\"isTouchscreen\":null}", `JSON.stringify(device)`)
305+
if (error) { return error }
306+
307+
return pass('ok')
308+
});
309+
}
310+

integration-tests/js-compute/fixtures/app/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import "./client.js"
1616
import "./config-store.js"
1717
import "./console.js"
1818
import "./crypto.js"
19+
import "./device.js"
1920
import "./dictionary.js"
2021
import "./dynamic-backend.js"
2122
import "./edge-rate-limiter.js"

0 commit comments

Comments
 (0)