diff options
Diffstat (limited to 'toolkit/components/normandy/test/unit')
20 files changed, 1459 insertions, 0 deletions
diff --git a/toolkit/components/normandy/test/unit/cookie_server.sjs b/toolkit/components/normandy/test/unit/cookie_server.sjs new file mode 100644 index 0000000000..ab6099f6a4 --- /dev/null +++ b/toolkit/components/normandy/test/unit/cookie_server.sjs @@ -0,0 +1,12 @@ +/** + * Sends responses that sets a cookie. + */ +function handleRequest(request, response) { + // Allow cross-origin, so you can XHR to it! + response.setHeader("Access-Control-Allow-Origin", "*", false); + // Avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + // Set a cookie + response.setHeader("Set-Cookie", "type=chocolate-chip", false); + response.write(""); +} diff --git a/toolkit/components/normandy/test/unit/echo_server.sjs b/toolkit/components/normandy/test/unit/echo_server.sjs new file mode 100644 index 0000000000..012f2b406e --- /dev/null +++ b/toolkit/components/normandy/test/unit/echo_server.sjs @@ -0,0 +1,21 @@ +/** + * Reads an HTTP status code and response body from the querystring and sends + * back a matching response. + */ +function handleRequest(request, response) { + // Allow cross-origin, so you can XHR to it! + response.setHeader("Access-Control-Allow-Origin", "*", false); + // Avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + const params = request.queryString.split("&"); + for (const param of params) { + const [key, value] = param.split("="); + if (key === "status") { + response.setStatusLine(null, value); + } else if (key === "body") { + response.write(value); + } + } + response.write(""); +} diff --git a/toolkit/components/normandy/test/unit/head_xpc.js b/toolkit/components/normandy/test/unit/head_xpc.js new file mode 100644 index 0000000000..ad2192be4b --- /dev/null +++ b/toolkit/components/normandy/test/unit/head_xpc.js @@ -0,0 +1,5 @@ +"use strict"; + +var { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); diff --git a/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/api/v1/index.json b/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/api/v1/index.json new file mode 100644 index 0000000000..5bef8d1302 --- /dev/null +++ b/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/api/v1/index.json @@ -0,0 +1,4 @@ +{ + "recipe-signed": "/api/v1/recipe/signed/", + "classify-client": "/api/v1/classify_client/" +} diff --git a/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/index.json b/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/index.json new file mode 100644 index 0000000000..d5495fa87f --- /dev/null +++ b/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/index.json @@ -0,0 +1,24 @@ +[ + { + "recipe": { + "action": "console-log", + "arguments": { "message": "this signature does not match this recipe" }, + "channels": [], + "countries": [], + "enabled": true, + "extra_filter_expression": "true || true", + "filter_expression": "true || true", + "id": 1, + "last_updated": "2017-02-17T18:29:09.839239Z", + "locales": [], + "name": "system-addon-test", + "revision_id": "b2cb8a26e132182d7d02cf50695d2c7f06cf3b954ff2ff63bca49d724ee91950" + }, + "signature": { + "public_key": "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p", + "signature": "p4g3eurmPsJK5UcGT97BRyKstpwZ_2mNJkDGpd6QXlkXfvgwprjeyb5yeIEkKUXqc6krWid4obB_OP9-CwOi9tvKY1pV8p98CT5BhF0IVgpF3b7KBW1a0BVdg5owoG5W", + "timestamp": "2017-02-17T18:29:09.847614Z", + "x5u": "/normandy.content-signature.mozilla.org-20210705.dev.chain" + } + } +] diff --git a/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/normandy.content-signature.mozilla.org-20210705.dev.chain b/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/normandy.content-signature.mozilla.org-20210705.dev.chain new file mode 100644 index 0000000000..5bf53787d8 --- /dev/null +++ b/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/normandy.content-signature.mozilla.org-20210705.dev.chain @@ -0,0 +1,123 @@ +-----BEGIN CERTIFICATE----- +MIIGRTCCBC2gAwIBAgIEAQAABTANBgkqhkiG9w0BAQwFADBrMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHQWxsaXpvbTEXMBUGA1UECxMOQ2xvdWQgU2VydmljZXMxMTAv +BgNVBAMTKERldnppbGxhIFNpZ25pbmcgU2VydmljZXMgSW50ZXJtZWRpYXRlIDEw +HhcNMTYwNzA2MjE1NzE1WhcNMjEwNzA1MjE1NzE1WjCBrzELMAkGA1UEBhMCVVMx +EzARBgNVBAgTCkNhbGlmb3JuaWExHDAaBgNVBAoTE01vemlsbGEgQ29ycG9yYXRp +b24xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMS8wLQYDVQQDEyZub3JtYW5keS5j +b250ZW50LXNpZ25hdHVyZS5tb3ppbGxhLm9yZzEjMCEGCSqGSIb3DQEJARYUc2Vj +dXJpdHlAbW96aWxsYS5vcmcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARUQqIIAiTB +GDVUWw/wk5h1IXpreq+BtE+gQr15O4tusHpCLGjOxwpHiJYnxk45fpE8JGAV19UO +hmqMUEU0k31C1EGTSZW0ducSvHrh3a8wXShZ6dxLWHItbbCGA6A7PumjggJYMIIC +VDAdBgNVHQ4EFgQUVfksSjlZ0i1TBiS1vcoObaMeXn0wge8GA1UdIwSB5zCB5IAU +/YboUIXAovChEpudDBuodHKbjUuhgcWkgcIwgb8xCzAJBgNVBAYTAlVTMQswCQYD +VQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVu +dCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNp +Z25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290 +Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIEAQAABDAMBgNVHRMBAf8EAjAA +MA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzBEBgNVHR8E +PTA7MDmgN6A1hjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3Mu +bmV0L2NhL2NybC5wZW0wQgYJYIZIAYb4QgEEBDUWM2h0dHBzOi8vY29udGVudC1z +aWduYXR1cmUuZGV2Lm1vemF3cy5uZXQvY2EvY3JsLnBlbTBOBggrBgEFBQcBAQRC +MEAwPgYIKwYBBQUHMAKGMmh0dHBzOi8vY29udGVudC1zaWduYXR1cmUuZGV2Lm1v +emF3cy5uZXQvY2EvY2EucGVtMDEGA1UdEQQqMCiCJm5vcm1hbmR5LmNvbnRlbnQt +c2lnbmF0dXJlLm1vemlsbGEub3JnMA0GCSqGSIb3DQEBDAUAA4ICAQCwb+8JTAB7 +ZfQmFqPUIV2cQQv696AaDPQCtA9YS4zmUfcLMvfZVAbK397zFr0RMDdLiTUQDoeq +rBEmPXhJRPiv6JAK4n7Jf6Y6XfXcNxx+q3garR09Vm/0CnEq/iV+ZAtPkoKIO9kr +Nkzecd894yQCF4hIuPQ5qtMySeqJmH3Dp13eq4T0Oew1Bu32rNHuBJh2xYBkWdun +aAw/YX0I5EqZBP/XA6gbiA160tTK+hnpnlMtw/ljkvfhHbWpICD4aSiTL8L3vABQ +j7bqjMKR5xDkuGWshZfcmonpvQhGTye/RZ1vz5IzA3VOJt1mz5bdZlitpaOm/Yv0 +x6aODz8GP/PiRWFQ5CW8Uf/7pGc5rSyvnfZV2ix8EzFlo8cUtuN1fjrPFPOFOLvG +iiB6S9nlXiKBGYIDdd8V8iC5xJpzjiAWJQigwSNzuc2K30+iPo3w0zazkwe5V8jW +gj6gItYxh5xwVQTPHD0EOd9HvV1ou42+rH5Y+ISFUm25zz02UtUHEK0BKtL0lmdt +DwVq5jcHn6bx2/iwUtlKvPXtfM/6JjTJlkLZLtS7U5/pwcS0owo9zAL0qg3bdm16 ++v/olmPqQFLUHmamJTzv3rojj5X/uVdx1HMM3wBjV9tRYoYaZw9RIInRmM8Z1pHv +JJ+CIZgCyd5vgp57BKiodRZcgHoCH+BkOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHijCCBXKgAwIBAgIEAQAABDANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQK +Ex1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNv +bnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2Vj +K2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjIx +NDkyNloXDTIxMDcwNTIxNDkyNlowazELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0Fs +bGl6b20xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMTEwLwYDVQQDEyhEZXZ6aWxs +YSBTaWduaW5nIFNlcnZpY2VzIEludGVybWVkaWF0ZSAxMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAypIfUURYgWzGw8G/1Pz9zW+Tsjirx2owThiv2gys +wJiWL/9/2gzKOrYDEqlDUudfA/BjVRtT9+NbYgnhaCkNfADOAacWS83aMhedAqhP +bVd5YhGQdpijI7f1AVTSb0ehrU2nhOZHvHX5Tk2fbRx3ryefIazNTLFGpiMBbsNv +tSI/+fjW8s0MhKNqlLnk6a9mZKo0mEy7HjGYV8nzsgI17rKLx/s2HG4TFG0+JQzc +UGlum3Tg58ritDzWdyKIkmKNZ48oLBX99Qc8j8B1UyiLv6TZmjVX0I+Ds7eSWHZk +0axLEpTyf2r7fHvN4iDNCPajw+ZpuuBfbs80Ha8b8MHvnpf9fbwiirodNQOVpY4c +t5E3Us3eYwBKdqDEbECWxCKGAS2/iVVUCNKHsg0sSxgqcwxrxyrddQRUQ0EM38DZ +F/vHt+vTdHt07kezbjJe0Kklel59uSpghA0iL4vxzbTns1fuwYOgVrNGs3acTkiB +GhFOxRXUPGtpdYmv+AaR9WlWJQY1GIEoVrinPVH7bcCwyh1CcUbHL+oAFTcmc6kZ +7azNg21tWILIRL7R0IZYQm0tF5TTwCsjVC7FuHaBtkxtVrrZqeKjvVXQ8TK5VoI0 +BUQ6BKHGeTtm+0HBpheYBDy3wkOsEGbGHLEM6cMeiz6PyCXF8wXli8Qb/TjN3LHZ +e30CAwEAAaOCAd8wggHbMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGG +MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBT9huhQhcCi8KESm50M +G6h0cpuNSzCB7AYDVR0jBIHkMIHhgBSDx8s0qJaMyQCehKcuzgzVNRA75qGBxaSB +wjCBvzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFp +biBWaWV3MSYwJAYDVQQKEx1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEm +MCQGA1UEAxMdZGV2LmNvbnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG +9w0BCQEWLGNsb3Vkc2VjK2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEu +Y29tggEBMEIGCWCGSAGG+EIBBAQ1FjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJl +LmRldi5tb3phd3MubmV0L2NhL2NybC5wZW0wTgYIKwYBBQUHAQEEQjBAMD4GCCsG +AQUFBzAChjJodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3MubmV0 +L2NhL2NhLnBlbTANBgkqhkiG9w0BAQwFAAOCAgEAbum0z0ccqI1Wp49VtsGmUPHA +gjPPy2Xa5NePmqY87WrGdhjN3xbLVb3hx8T2N6pqDjMY2rEynXKEOek3oJkQ3C6J +8AFP6Y93gaAlNz6EA0J0mqdW5TMI8YEYsu2ma+dQQ8wm9f/5b+/Y8qwfhztP06W5 +H6IG04/RvgUwYMnSR4QvT309fu5UmCRUDzsO53ZmQCfmN94u3NxHc4S6n0Q6AKAM +kh5Ld9SQnlqqqDykzn7hYDi8nTLWc7IYqkGfNMilDEKbAl4CjnSfyEvpdFAJ9sPR +UL+kaWFSMvaqIPNpxS5OpoPZjmxEc9HHnCHxtfDHWdXTJILjijPrCdMaxOCHfIqV +5roOJggI4RZ0YM68IL1MfN4IEVOrHhKjDHtd1gcmy2KU4jfj9LQq9YTnyvZ2z1yS +lS310HG3or1K8Nnu5Utfe7T6ppX8bLRMkS1/w0p7DKxHaf4D/GJcCtM9lcSt9JpW +6ACKFikjWR4ZxczYKgApc0wcjd7XBuO5777xtOeyEUDHdDft3jiXA91dYM5UAzc3 +69z/3zmaELzo0gWcrjLXh7fU9AvbU4EUF6rwzxbPGF78jJcGK+oBf8uWUCkBykDt +VsAEZI1u4EDg8e/C1nFqaH9gNMArAgquYIB9rve+hdprIMnva0S147pflWopBWcb +jwzgpfquuYnnxe0CNBA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH3DCCBcSgAwIBAgIBATANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQKEx1D +b250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNvbnRl +bnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2VjK2Rl +dnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjE4MTUy +MloXDTI2MDcwNDE4MTUyMlowgb8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEW +MBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVudCBTaWduYXR1 +cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNpZ25hdHVyZS5y +b290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290Y29udGVudHNp +Z25hdHVyZUBtb3ppbGxhLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAJcPcXhD8MzF6OTn5qZ0L7lX1+PEgLKhI9g1HxxDYDVup4Zm0kZhPGmFSlml +6eVO99OvvHdAlHhQGCIG7h+w1cp66mWjfpcvtQH23uRoKZfiW3jy1jUWrvdXolxR +t1taZosjzo+9OP8TvG6LpJj7AvqUiYD4wYnQJtt0jNRN4d6MUfQwiavSS5uTBuxd +ZJ4TsPvEI+Iv4A4PSobSzxkg79LTMvsGtDLQv7nN5hMs9T18EL5GnIKoqnSQCU0d +n2CN7S3QiQ+cbORWsSYqCTj1gUbFh6X3duEB/ypLcyWFbqeJmPHikjY8q8pLjZSB +IYiTJYLyvYlKdM5QleC/xuBNnMPCftrwwLHjWE4Dd7C9t7k0R5xyOetuiHLCwOcQ +tuckp7RgFKoviMNv3gdkzwVapOklcsaRkRscv6OMTKJNsdJVIDLrPF1wMABhbEQB +64BL0uL4lkFtpXXbJzQ6mgUNQveJkfUWOoB+cA/6GtI4J0aQfvQgloCYI6jxNn/e +Nvk5OV9KFOhXS2dnDft3wHU46sg5yXOuds1u6UrOoATBNFlkS95m4zIX1Svu091+ +CKTiLK85+ZiFtAlU2bPr3Bk3GhL3Z586ae6a4QUEx6SPQVXc18ezB4qxKqFc+avI +ylccYMRhVP+ruADxtUM5Vy6x3U8BwBK5RLdecRx2FEBDImY1AgMBAAGjggHfMIIB +2zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAWBgNVHSUBAf8EDDAK +BggrBgEFBQcDAzAdBgNVHQ4EFgQUg8fLNKiWjMkAnoSnLs4M1TUQO+YwgewGA1Ud +IwSB5DCB4YAUg8fLNKiWjMkAnoSnLs4M1TUQO+ahgcWkgcIwgb8xCzAJBgNVBAYT +AlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UE +ChMdQ29udGVudCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5j +b250ZW50LXNpZ25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNl +YytkZXZyb290Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIBATBCBglghkgB +hvhCAQQENRYzaHR0cHM6Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5l +dC9jYS9jcmwucGVtME4GCCsGAQUFBwEBBEIwQDA+BggrBgEFBQcwAoYyaHR0cHM6 +Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5ldC9jYS9jYS5wZW0wDQYJ +KoZIhvcNAQEMBQADggIBAAAQ+fotZE79FfZ8Lz7eiTUzlwHXCdSE2XD3nMROu6n6 +uLTBPrf2C+k+U1FjKVvL5/WCUj6hIzP2X6Sb8+o0XHX9mKN0yoMORTEYJOnazYPK +KSUUFnE4vGgQkr6k/31gGRMTICdnf3VOOAlUCQ5bOmGIuWi401E3sbd85U+TJe0A +nHlU+XjtfzlqcBvQivdbA0s+GEG55uRPvn952aTBEMHfn+2JqKeLShl4AtUAfu+h +6md3Z2HrEC7B3GK8ekWPu0G/ZuWTuFvOimZ+5C8IPRJXcIR/siPQl1x6dpTCew6t +lPVcVuvg6SQwzvxetkNrGUe2Wb2s9+PK2PUvfOS8ee25SNmfG3XK9qJpqGUhzSBX +T8QQgyxd0Su5G7Wze7aaHZd/fqIm/G8YFR0HiC2xni/lnDTXFDPCe+HCnSk0bH6U +wpr6I1yK8oZ2IdnNVfuABGMmGOhvSQ8r7//ea9WKhCsGNQawpVWVioY7hpyNAJ0O +Vn4xqG5f6allz8lgpwAQ+AeEEClHca6hh6mj9KhD1Of1CC2Vx52GHNh/jMYEc3/g +zLKniencBqn3Y2XH2daITGJddcleN09+a1NaTkT3hgr7LumxM8EVssPkC+z9j4Vf +Gbste+8S5QCMhh00g5vR9QF8EaFqdxCdSxrsA4GmpCa5UQl8jtCnpp2DLKXuOh72 +-----END CERTIFICATE----- diff --git a/toolkit/components/normandy/test/unit/mock_api/api/v1/classify_client/index.json b/toolkit/components/normandy/test/unit/mock_api/api/v1/classify_client/index.json new file mode 100644 index 0000000000..a9b6239e48 --- /dev/null +++ b/toolkit/components/normandy/test/unit/mock_api/api/v1/classify_client/index.json @@ -0,0 +1,4 @@ +{ + "country": "US", + "request_time": "2017-02-22T17:43:24.657841Z" +} diff --git a/toolkit/components/normandy/test/unit/mock_api/api/v1/extension/1/index.json b/toolkit/components/normandy/test/unit/mock_api/api/v1/extension/1/index.json new file mode 100644 index 0000000000..f088592a9b --- /dev/null +++ b/toolkit/components/normandy/test/unit/mock_api/api/v1/extension/1/index.json @@ -0,0 +1,9 @@ +{ + "id": 1, + "name": "Normandy Fixture", + "xpi": "http://example.com/browser/toolkit/components/normandy/test/browser/fixtures/normandy.xpi", + "extension_id": "normandydriver@example.com", + "version": "1.0", + "hash": "ade1c14196ec4fe0aa0a6ba40ac433d7c8d1ec985581a8a94d43dc58991b5171", + "hash_algorithm": "sha256" +} diff --git a/toolkit/components/normandy/test/unit/mock_api/api/v1/extension/index.json b/toolkit/components/normandy/test/unit/mock_api/api/v1/extension/index.json new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/toolkit/components/normandy/test/unit/mock_api/api/v1/extension/index.json diff --git a/toolkit/components/normandy/test/unit/mock_api/api/v1/index.json b/toolkit/components/normandy/test/unit/mock_api/api/v1/index.json new file mode 100644 index 0000000000..d2414056c0 --- /dev/null +++ b/toolkit/components/normandy/test/unit/mock_api/api/v1/index.json @@ -0,0 +1,5 @@ +{ + "classify-client": "/api/v1/classify_client/", + "extension-list": "/api/v1/extension/", + "recipe-signed": "/api/v1/recipe/signed/" +} diff --git a/toolkit/components/normandy/test/unit/mock_api/api/v1/recipe/signed/index.json b/toolkit/components/normandy/test/unit/mock_api/api/v1/recipe/signed/index.json new file mode 100644 index 0000000000..5f3515dc97 --- /dev/null +++ b/toolkit/components/normandy/test/unit/mock_api/api/v1/recipe/signed/index.json @@ -0,0 +1,24 @@ +[ + { + "recipe": { + "action": "console-log", + "arguments": { "message": "asdfasfda sdf sa" }, + "channels": [], + "countries": [], + "enabled": true, + "extra_filter_expression": "true || true", + "filter_expression": "true || true", + "id": 1, + "last_updated": "2017-02-17T18:29:09.839239Z", + "locales": [], + "name": "system-addon-test", + "revision_id": "b2cb8a26e132182d7d02cf50695d2c7f06cf3b954ff2ff63bca49d724ee91950" + }, + "signature": { + "public_key": "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVEKiCAIkwRg1VFsP8JOYdSF6a3qvgbRPoEK9eTuLbrB6QixozscKR4iWJ8ZOOX6RPCRgFdfVDoZqjFBFNJN9QtRBk0mVtHbnErx64d2vMF0oWencS1hyLW2whgOgOz7p", + "signature": "p4g3eurmPsJK5UcGT97BRyKstpwZ_2mNJkDGpd6QXlkXfvgwprjeyb5yeIEkKUXqc6krWid4obB_OP9-CwOi9tvKY1pV8p98CT5BhF0IVgpF3b7KBW1a0BVdg5owoG5W", + "timestamp": "2017-02-17T18:29:09.847614Z", + "x5u": "/normandy.content-signature.mozilla.org-20210705.dev.chain" + } + } +] diff --git a/toolkit/components/normandy/test/unit/mock_api/normandy.content-signature.mozilla.org-20210705.dev.chain b/toolkit/components/normandy/test/unit/mock_api/normandy.content-signature.mozilla.org-20210705.dev.chain new file mode 100644 index 0000000000..5bf53787d8 --- /dev/null +++ b/toolkit/components/normandy/test/unit/mock_api/normandy.content-signature.mozilla.org-20210705.dev.chain @@ -0,0 +1,123 @@ +-----BEGIN CERTIFICATE----- +MIIGRTCCBC2gAwIBAgIEAQAABTANBgkqhkiG9w0BAQwFADBrMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHQWxsaXpvbTEXMBUGA1UECxMOQ2xvdWQgU2VydmljZXMxMTAv +BgNVBAMTKERldnppbGxhIFNpZ25pbmcgU2VydmljZXMgSW50ZXJtZWRpYXRlIDEw +HhcNMTYwNzA2MjE1NzE1WhcNMjEwNzA1MjE1NzE1WjCBrzELMAkGA1UEBhMCVVMx +EzARBgNVBAgTCkNhbGlmb3JuaWExHDAaBgNVBAoTE01vemlsbGEgQ29ycG9yYXRp +b24xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMS8wLQYDVQQDEyZub3JtYW5keS5j +b250ZW50LXNpZ25hdHVyZS5tb3ppbGxhLm9yZzEjMCEGCSqGSIb3DQEJARYUc2Vj +dXJpdHlAbW96aWxsYS5vcmcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARUQqIIAiTB +GDVUWw/wk5h1IXpreq+BtE+gQr15O4tusHpCLGjOxwpHiJYnxk45fpE8JGAV19UO +hmqMUEU0k31C1EGTSZW0ducSvHrh3a8wXShZ6dxLWHItbbCGA6A7PumjggJYMIIC +VDAdBgNVHQ4EFgQUVfksSjlZ0i1TBiS1vcoObaMeXn0wge8GA1UdIwSB5zCB5IAU +/YboUIXAovChEpudDBuodHKbjUuhgcWkgcIwgb8xCzAJBgNVBAYTAlVTMQswCQYD +VQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVu +dCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNp +Z25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290 +Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIEAQAABDAMBgNVHRMBAf8EAjAA +MA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzBEBgNVHR8E +PTA7MDmgN6A1hjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3Mu +bmV0L2NhL2NybC5wZW0wQgYJYIZIAYb4QgEEBDUWM2h0dHBzOi8vY29udGVudC1z +aWduYXR1cmUuZGV2Lm1vemF3cy5uZXQvY2EvY3JsLnBlbTBOBggrBgEFBQcBAQRC +MEAwPgYIKwYBBQUHMAKGMmh0dHBzOi8vY29udGVudC1zaWduYXR1cmUuZGV2Lm1v +emF3cy5uZXQvY2EvY2EucGVtMDEGA1UdEQQqMCiCJm5vcm1hbmR5LmNvbnRlbnQt +c2lnbmF0dXJlLm1vemlsbGEub3JnMA0GCSqGSIb3DQEBDAUAA4ICAQCwb+8JTAB7 +ZfQmFqPUIV2cQQv696AaDPQCtA9YS4zmUfcLMvfZVAbK397zFr0RMDdLiTUQDoeq +rBEmPXhJRPiv6JAK4n7Jf6Y6XfXcNxx+q3garR09Vm/0CnEq/iV+ZAtPkoKIO9kr +Nkzecd894yQCF4hIuPQ5qtMySeqJmH3Dp13eq4T0Oew1Bu32rNHuBJh2xYBkWdun +aAw/YX0I5EqZBP/XA6gbiA160tTK+hnpnlMtw/ljkvfhHbWpICD4aSiTL8L3vABQ +j7bqjMKR5xDkuGWshZfcmonpvQhGTye/RZ1vz5IzA3VOJt1mz5bdZlitpaOm/Yv0 +x6aODz8GP/PiRWFQ5CW8Uf/7pGc5rSyvnfZV2ix8EzFlo8cUtuN1fjrPFPOFOLvG +iiB6S9nlXiKBGYIDdd8V8iC5xJpzjiAWJQigwSNzuc2K30+iPo3w0zazkwe5V8jW +gj6gItYxh5xwVQTPHD0EOd9HvV1ou42+rH5Y+ISFUm25zz02UtUHEK0BKtL0lmdt +DwVq5jcHn6bx2/iwUtlKvPXtfM/6JjTJlkLZLtS7U5/pwcS0owo9zAL0qg3bdm16 ++v/olmPqQFLUHmamJTzv3rojj5X/uVdx1HMM3wBjV9tRYoYaZw9RIInRmM8Z1pHv +JJ+CIZgCyd5vgp57BKiodRZcgHoCH+BkOQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIHijCCBXKgAwIBAgIEAQAABDANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQK +Ex1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNv +bnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2Vj +K2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjIx +NDkyNloXDTIxMDcwNTIxNDkyNlowazELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0Fs +bGl6b20xFzAVBgNVBAsTDkNsb3VkIFNlcnZpY2VzMTEwLwYDVQQDEyhEZXZ6aWxs +YSBTaWduaW5nIFNlcnZpY2VzIEludGVybWVkaWF0ZSAxMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAypIfUURYgWzGw8G/1Pz9zW+Tsjirx2owThiv2gys +wJiWL/9/2gzKOrYDEqlDUudfA/BjVRtT9+NbYgnhaCkNfADOAacWS83aMhedAqhP +bVd5YhGQdpijI7f1AVTSb0ehrU2nhOZHvHX5Tk2fbRx3ryefIazNTLFGpiMBbsNv +tSI/+fjW8s0MhKNqlLnk6a9mZKo0mEy7HjGYV8nzsgI17rKLx/s2HG4TFG0+JQzc +UGlum3Tg58ritDzWdyKIkmKNZ48oLBX99Qc8j8B1UyiLv6TZmjVX0I+Ds7eSWHZk +0axLEpTyf2r7fHvN4iDNCPajw+ZpuuBfbs80Ha8b8MHvnpf9fbwiirodNQOVpY4c +t5E3Us3eYwBKdqDEbECWxCKGAS2/iVVUCNKHsg0sSxgqcwxrxyrddQRUQ0EM38DZ +F/vHt+vTdHt07kezbjJe0Kklel59uSpghA0iL4vxzbTns1fuwYOgVrNGs3acTkiB +GhFOxRXUPGtpdYmv+AaR9WlWJQY1GIEoVrinPVH7bcCwyh1CcUbHL+oAFTcmc6kZ +7azNg21tWILIRL7R0IZYQm0tF5TTwCsjVC7FuHaBtkxtVrrZqeKjvVXQ8TK5VoI0 +BUQ6BKHGeTtm+0HBpheYBDy3wkOsEGbGHLEM6cMeiz6PyCXF8wXli8Qb/TjN3LHZ +e30CAwEAAaOCAd8wggHbMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGG +MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBT9huhQhcCi8KESm50M +G6h0cpuNSzCB7AYDVR0jBIHkMIHhgBSDx8s0qJaMyQCehKcuzgzVNRA75qGBxaSB +wjCBvzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFp +biBWaWV3MSYwJAYDVQQKEx1Db250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEm +MCQGA1UEAxMdZGV2LmNvbnRlbnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG +9w0BCQEWLGNsb3Vkc2VjK2RldnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEu +Y29tggEBMEIGCWCGSAGG+EIBBAQ1FjNodHRwczovL2NvbnRlbnQtc2lnbmF0dXJl +LmRldi5tb3phd3MubmV0L2NhL2NybC5wZW0wTgYIKwYBBQUHAQEEQjBAMD4GCCsG +AQUFBzAChjJodHRwczovL2NvbnRlbnQtc2lnbmF0dXJlLmRldi5tb3phd3MubmV0 +L2NhL2NhLnBlbTANBgkqhkiG9w0BAQwFAAOCAgEAbum0z0ccqI1Wp49VtsGmUPHA +gjPPy2Xa5NePmqY87WrGdhjN3xbLVb3hx8T2N6pqDjMY2rEynXKEOek3oJkQ3C6J +8AFP6Y93gaAlNz6EA0J0mqdW5TMI8YEYsu2ma+dQQ8wm9f/5b+/Y8qwfhztP06W5 +H6IG04/RvgUwYMnSR4QvT309fu5UmCRUDzsO53ZmQCfmN94u3NxHc4S6n0Q6AKAM +kh5Ld9SQnlqqqDykzn7hYDi8nTLWc7IYqkGfNMilDEKbAl4CjnSfyEvpdFAJ9sPR +UL+kaWFSMvaqIPNpxS5OpoPZjmxEc9HHnCHxtfDHWdXTJILjijPrCdMaxOCHfIqV +5roOJggI4RZ0YM68IL1MfN4IEVOrHhKjDHtd1gcmy2KU4jfj9LQq9YTnyvZ2z1yS +lS310HG3or1K8Nnu5Utfe7T6ppX8bLRMkS1/w0p7DKxHaf4D/GJcCtM9lcSt9JpW +6ACKFikjWR4ZxczYKgApc0wcjd7XBuO5777xtOeyEUDHdDft3jiXA91dYM5UAzc3 +69z/3zmaELzo0gWcrjLXh7fU9AvbU4EUF6rwzxbPGF78jJcGK+oBf8uWUCkBykDt +VsAEZI1u4EDg8e/C1nFqaH9gNMArAgquYIB9rve+hdprIMnva0S147pflWopBWcb +jwzgpfquuYnnxe0CNBA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIH3DCCBcSgAwIBAgIBATANBgkqhkiG9w0BAQwFADCBvzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MSYwJAYDVQQKEx1D +b250ZW50IFNpZ25hdHVyZSBEZXYgU2lnbmluZzEmMCQGA1UEAxMdZGV2LmNvbnRl +bnQtc2lnbmF0dXJlLnJvb3QuY2ExOzA5BgkqhkiG9w0BCQEWLGNsb3Vkc2VjK2Rl +dnJvb3Rjb250ZW50c2lnbmF0dXJlQG1vemlsbGEuY29tMB4XDTE2MDcwNjE4MTUy +MloXDTI2MDcwNDE4MTUyMlowgb8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEW +MBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UEChMdQ29udGVudCBTaWduYXR1 +cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5jb250ZW50LXNpZ25hdHVyZS5y +b290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNlYytkZXZyb290Y29udGVudHNp +Z25hdHVyZUBtb3ppbGxhLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAJcPcXhD8MzF6OTn5qZ0L7lX1+PEgLKhI9g1HxxDYDVup4Zm0kZhPGmFSlml +6eVO99OvvHdAlHhQGCIG7h+w1cp66mWjfpcvtQH23uRoKZfiW3jy1jUWrvdXolxR +t1taZosjzo+9OP8TvG6LpJj7AvqUiYD4wYnQJtt0jNRN4d6MUfQwiavSS5uTBuxd +ZJ4TsPvEI+Iv4A4PSobSzxkg79LTMvsGtDLQv7nN5hMs9T18EL5GnIKoqnSQCU0d +n2CN7S3QiQ+cbORWsSYqCTj1gUbFh6X3duEB/ypLcyWFbqeJmPHikjY8q8pLjZSB +IYiTJYLyvYlKdM5QleC/xuBNnMPCftrwwLHjWE4Dd7C9t7k0R5xyOetuiHLCwOcQ +tuckp7RgFKoviMNv3gdkzwVapOklcsaRkRscv6OMTKJNsdJVIDLrPF1wMABhbEQB +64BL0uL4lkFtpXXbJzQ6mgUNQveJkfUWOoB+cA/6GtI4J0aQfvQgloCYI6jxNn/e +Nvk5OV9KFOhXS2dnDft3wHU46sg5yXOuds1u6UrOoATBNFlkS95m4zIX1Svu091+ +CKTiLK85+ZiFtAlU2bPr3Bk3GhL3Z586ae6a4QUEx6SPQVXc18ezB4qxKqFc+avI +ylccYMRhVP+ruADxtUM5Vy6x3U8BwBK5RLdecRx2FEBDImY1AgMBAAGjggHfMIIB +2zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAWBgNVHSUBAf8EDDAK +BggrBgEFBQcDAzAdBgNVHQ4EFgQUg8fLNKiWjMkAnoSnLs4M1TUQO+YwgewGA1Ud +IwSB5DCB4YAUg8fLNKiWjMkAnoSnLs4M1TUQO+ahgcWkgcIwgb8xCzAJBgNVBAYT +AlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEmMCQGA1UE +ChMdQ29udGVudCBTaWduYXR1cmUgRGV2IFNpZ25pbmcxJjAkBgNVBAMTHWRldi5j +b250ZW50LXNpZ25hdHVyZS5yb290LmNhMTswOQYJKoZIhvcNAQkBFixjbG91ZHNl +YytkZXZyb290Y29udGVudHNpZ25hdHVyZUBtb3ppbGxhLmNvbYIBATBCBglghkgB +hvhCAQQENRYzaHR0cHM6Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5l +dC9jYS9jcmwucGVtME4GCCsGAQUFBwEBBEIwQDA+BggrBgEFBQcwAoYyaHR0cHM6 +Ly9jb250ZW50LXNpZ25hdHVyZS5kZXYubW96YXdzLm5ldC9jYS9jYS5wZW0wDQYJ +KoZIhvcNAQEMBQADggIBAAAQ+fotZE79FfZ8Lz7eiTUzlwHXCdSE2XD3nMROu6n6 +uLTBPrf2C+k+U1FjKVvL5/WCUj6hIzP2X6Sb8+o0XHX9mKN0yoMORTEYJOnazYPK +KSUUFnE4vGgQkr6k/31gGRMTICdnf3VOOAlUCQ5bOmGIuWi401E3sbd85U+TJe0A +nHlU+XjtfzlqcBvQivdbA0s+GEG55uRPvn952aTBEMHfn+2JqKeLShl4AtUAfu+h +6md3Z2HrEC7B3GK8ekWPu0G/ZuWTuFvOimZ+5C8IPRJXcIR/siPQl1x6dpTCew6t +lPVcVuvg6SQwzvxetkNrGUe2Wb2s9+PK2PUvfOS8ee25SNmfG3XK9qJpqGUhzSBX +T8QQgyxd0Su5G7Wze7aaHZd/fqIm/G8YFR0HiC2xni/lnDTXFDPCe+HCnSk0bH6U +wpr6I1yK8oZ2IdnNVfuABGMmGOhvSQ8r7//ea9WKhCsGNQawpVWVioY7hpyNAJ0O +Vn4xqG5f6allz8lgpwAQ+AeEEClHca6hh6mj9KhD1Of1CC2Vx52GHNh/jMYEc3/g +zLKniencBqn3Y2XH2daITGJddcleN09+a1NaTkT3hgr7LumxM8EVssPkC+z9j4Vf +Gbste+8S5QCMhh00g5vR9QF8EaFqdxCdSxrsA4GmpCa5UQl8jtCnpp2DLKXuOh72 +-----END CERTIFICATE----- diff --git a/toolkit/components/normandy/test/unit/query_server.sjs b/toolkit/components/normandy/test/unit/query_server.sjs new file mode 100644 index 0000000000..dd00d74bf6 --- /dev/null +++ b/toolkit/components/normandy/test/unit/query_server.sjs @@ -0,0 +1,34 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +// Returns a JSON string containing the query string arguments and the +// request body parsed as JSON. +function handleRequest(request, response) { + // Allow cross-origin, so you can XHR to it! + response.setHeader("Access-Control-Allow-Origin", "*", false); + // Avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/json", false); + + // Read request body + const inputStream = new BinaryInputStream(request.bodyInputStream); + let bytes = []; + let available; + while ((available = inputStream.available()) > 0) { + bytes = bytes.concat(inputStream.readByteArray(available)); + } + const body = String.fromCharCode.apply(null, bytes); + + // Write response body + const data = { queryString: {}, body: body ? JSON.parse(body) : {} }; + const params = request.queryString.split("&"); + for (const param of params) { + const [key, value] = param.split("="); + data.queryString[key] = value; + } + response.write(JSON.stringify(data)); +} diff --git a/toolkit/components/normandy/test/unit/test_Normandy.js b/toolkit/components/normandy/test/unit/test_Normandy.js new file mode 100644 index 0000000000..5bb1655fb8 --- /dev/null +++ b/toolkit/components/normandy/test/unit/test_Normandy.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { Normandy } = ChromeUtils.importESModule( + "resource://normandy/Normandy.sys.mjs" +); +const { NormandyMigrations } = ChromeUtils.importESModule( + "resource://normandy/NormandyMigrations.sys.mjs" +); +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); +ChromeUtils.defineESModuleGetters(this, { + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +/* import-globals-from utils.js */ +load("utils.js"); + +NormandyTestUtils.init({ add_task }); +const { decorate_task } = NormandyTestUtils; + +// Normandy's initialization function should set the start preferences before +// its first `await`. +decorate_task( + NormandyTestUtils.withStub(Normandy, "finishInit"), + NormandyTestUtils.withStub(NormandyMigrations, "applyAll"), + NormandyTestUtils.withMockPreferences(), + async function test_normandy_init_applies_startup_prefs_synchronously({ + mockPreferences, + }) { + const experimentPref = "test.experiment"; + const rolloutPref = "test.rollout"; + const experimentStartupPref = `app.normandy.startupExperimentPrefs.${experimentPref}`; + const rolloutStartupPref = `app.normandy.startupRolloutPrefs.${rolloutPref}`; + + mockPreferences.preserve(experimentPref, "default"); + mockPreferences.preserve(rolloutPref, "default"); + mockPreferences.set(experimentStartupPref, "experiment"); + mockPreferences.set(rolloutStartupPref, "rollout"); + + Assert.equal( + Services.prefs.getCharPref(experimentPref, "default"), + "default" + ); + Assert.equal(Services.prefs.getCharPref(rolloutPref, "default"), "default"); + + let initPromise = Normandy.init({ runAsync: false }); + + // note: There are no awaits before these asserts, so only the part of + // Normandy's initialization before its first await can run. + Assert.equal( + Services.prefs.getCharPref(experimentPref, "default"), + "experiment" + ); + Assert.equal(Services.prefs.getCharPref(rolloutPref, "default"), "rollout"); + + await initPromise; + await Normandy.uninit(); + } +); + +// Normandy's initialization function should register the observer for UI +// startup before it's first await. +decorate_task( + NormandyTestUtils.withStub(Normandy, "finishInit"), + NormandyTestUtils.withStub(NormandyMigrations, "applyAll"), + async function test_normandy_init_applies_startup_prefs_synchronously({ + applyAllStub, + }) { + let originalDeferred = Normandy.uiAvailableNotificationObserved; + let mockUiAvailableDeferred = PromiseUtils.defer(); + Normandy.uiAvailableNotificationObserved = mockUiAvailableDeferred; + + let applyAllDeferred = PromiseUtils.defer(); + applyAllStub.returns(applyAllStub); + + let promiseResolvedCount = 0; + mockUiAvailableDeferred.promise.then(() => promiseResolvedCount++); + + let initPromise = Normandy.init(); + + Assert.equal(promiseResolvedCount, 0); + Normandy.observe(null, "sessionstore-windows-restored"); + await TestUtils.waitForCondition(() => promiseResolvedCount === 1); + + applyAllDeferred.resolve(); + + await initPromise; + await Normandy.uninit(); + Normandy.uiAvailableNotificationObserved = originalDeferred; + } +); diff --git a/toolkit/components/normandy/test/unit/test_NormandyApi.js b/toolkit/components/normandy/test/unit/test_NormandyApi.js new file mode 100644 index 0000000000..885bd9fbdb --- /dev/null +++ b/toolkit/components/normandy/test/unit/test_NormandyApi.js @@ -0,0 +1,257 @@ +/* globals sinon */ +"use strict"; + +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +/* import-globals-from utils.js */ +load("utils.js"); + +NormandyTestUtils.init({ add_task }); +const { decorate_task } = NormandyTestUtils; + +Cu.importGlobalProperties(["fetch"]); + +decorate_task(withMockApiServer(), async function test_get({ serverUrl }) { + // Test that NormandyApi can fetch from the test server. + const response = await NormandyApi.get(`${serverUrl}/api/v1/`); + const data = await response.json(); + equal( + data["recipe-signed"], + "/api/v1/recipe/signed/", + "Expected data in response" + ); +}); + +decorate_task( + withMockApiServer(), + async function test_getApiUrl({ serverUrl }) { + const apiBase = `${serverUrl}/api/v1`; + // Test that NormandyApi can use the self-describing API's index + const recipeListUrl = await NormandyApi.getApiUrl("extension-list"); + equal( + recipeListUrl, + `${apiBase}/extension/`, + "Can retrieve extension-list URL from API" + ); + } +); + +decorate_task( + withMockApiServer(), + async function test_getApiUrlSlashes({ serverUrl, mockPreferences }) { + const fakeResponse = new MockResponse( + JSON.stringify({ "test-endpoint": `${serverUrl}/test/` }) + ); + const mockGet = sinon + .stub(NormandyApi, "get") + .callsFake(async () => fakeResponse); + + // without slash + { + NormandyApi.clearIndexCache(); + mockPreferences.set("app.normandy.api_url", `${serverUrl}/api/v1`); + const endpoint = await NormandyApi.getApiUrl("test-endpoint"); + equal(endpoint, `${serverUrl}/test/`); + ok( + mockGet.calledWithExactly(`${serverUrl}/api/v1/`), + "trailing slash was added" + ); + mockGet.resetHistory(); + } + + // with slash + { + NormandyApi.clearIndexCache(); + mockPreferences.set("app.normandy.api_url", `${serverUrl}/api/v1/`); + const endpoint = await NormandyApi.getApiUrl("test-endpoint"); + equal(endpoint, `${serverUrl}/test/`); + ok( + mockGet.calledWithExactly(`${serverUrl}/api/v1/`), + "existing trailing slash was preserved" + ); + mockGet.resetHistory(); + } + + NormandyApi.clearIndexCache(); + mockGet.restore(); + } +); + +// Test validation errors due to validation throwing an exception (e.g. when +// parameters passed to validation are malformed). +decorate_task( + withMockApiServer(), + async function test_validateSignedObject_validation_error() { + // Mock the x5u URL + const getStub = sinon.stub(NormandyApi, "get").callsFake(async url => { + ok(url.endsWith("x5u/"), "the only request should be to fetch the x5u"); + return new MockResponse("certchain"); + }); + + const signedObject = { a: 1, b: 2 }; + const signature = { + signature: "invalidsignature", + x5u: "http://localhost/x5u/", + }; + + // Validation should fail due to a malformed x5u and signature. + try { + await NormandyApi.verifyObjectSignature( + signedObject, + signature, + "object" + ); + ok(false, "validateSignedObject did not throw for a validation error"); + } catch (err) { + ok( + err instanceof NormandyApi.InvalidSignatureError, + "Error is an InvalidSignatureError" + ); + ok(/signature/.test(err), "Error is due to a validation error"); + } + + getStub.restore(); + } +); + +// Test validation errors due to validation returning false (e.g. when parameters +// passed to validation are correctly formed, but not valid for the data). +decorate_task( + withMockApiServer("invalid_recipe_signature_api"), + async function test_verifySignedObject_invalid_signature() { + // Get the test recipe and signature from the mock server. + const recipesUrl = await NormandyApi.getApiUrl("recipe-signed"); + const recipeResponse = await NormandyApi.get(recipesUrl); + const recipes = await recipeResponse.json(); + equal(recipes.length, 1, "Test data has one recipe"); + const [{ recipe, signature }] = recipes; + + try { + await NormandyApi.verifyObjectSignature(recipe, signature, "recipe"); + ok(false, "verifyObjectSignature did not throw for an invalid signature"); + } catch (err) { + ok( + err instanceof NormandyApi.InvalidSignatureError, + "Error is an InvalidSignatureError" + ); + ok(/signature/.test(err), "Error is due to an invalid signature"); + } + } +); + +decorate_task(withMockApiServer(), async function test_classifyClient() { + const classification = await NormandyApi.classifyClient(); + Assert.deepEqual(classification, { + country: "US", + request_time: new Date("2017-02-22T17:43:24.657841Z"), + }); +}); + +decorate_task(withMockApiServer(), async function test_fetchExtensionDetails() { + const extensionDetails = await NormandyApi.fetchExtensionDetails(1); + deepEqual(extensionDetails, { + id: 1, + name: "Normandy Fixture", + xpi: "http://example.com/browser/toolkit/components/normandy/test/browser/fixtures/normandy.xpi", + extension_id: "normandydriver@example.com", + version: "1.0", + hash: "ade1c14196ec4fe0aa0a6ba40ac433d7c8d1ec985581a8a94d43dc58991b5171", + hash_algorithm: "sha256", + }); +}); + +decorate_task( + withScriptServer("query_server.sjs"), + async function test_getTestServer({ serverUrl }) { + // Test that NormandyApi can fetch from the test server. + const response = await NormandyApi.get(serverUrl); + const data = await response.json(); + Assert.deepEqual( + data, + { queryString: {}, body: {} }, + "NormandyApi returned incorrect server data." + ); + } +); + +decorate_task( + withScriptServer("query_server.sjs"), + async function test_getQueryString({ serverUrl }) { + // Test that NormandyApi can send query string parameters to the test server. + const response = await NormandyApi.get(serverUrl, { + foo: "bar", + baz: "biff", + }); + const data = await response.json(); + Assert.deepEqual( + data, + { queryString: { foo: "bar", baz: "biff" }, body: {} }, + "NormandyApi sent an incorrect query string." + ); + } +); + +// Test that no credentials are sent, even if the cookie store contains them. +decorate_task( + withScriptServer("cookie_server.sjs"), + async function test_sendsNoCredentials({ serverUrl }) { + // This test uses cookie_server.sjs, which responds to all requests with a + // response that sets a cookie. + + // send a request, to store a cookie in the cookie store + await fetch(serverUrl); + + // A normal request should send that cookie + const cookieExpectedDeferred = PromiseUtils.defer(); + function cookieExpectedObserver(aSubject, aTopic, aData) { + equal( + aTopic, + "http-on-modify-request", + "Only the expected topic should be observed" + ); + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + equal( + httpChannel.getRequestHeader("Cookie"), + "type=chocolate-chip", + "The header should be sent" + ); + Services.obs.removeObserver( + cookieExpectedObserver, + "http-on-modify-request" + ); + cookieExpectedDeferred.resolve(); + } + Services.obs.addObserver(cookieExpectedObserver, "http-on-modify-request"); + await fetch(serverUrl); + await cookieExpectedDeferred.promise; + + // A request through the NormandyApi method should not send that cookie + const cookieNotExpectedDeferred = PromiseUtils.defer(); + function cookieNotExpectedObserver(aSubject, aTopic, aData) { + equal( + aTopic, + "http-on-modify-request", + "Only the expected topic should be observed" + ); + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + Assert.throws( + () => httpChannel.getRequestHeader("Cookie"), + /NS_ERROR_NOT_AVAILABLE/, + "The cookie header should not be sent" + ); + Services.obs.removeObserver( + cookieNotExpectedObserver, + "http-on-modify-request" + ); + cookieNotExpectedDeferred.resolve(); + } + Services.obs.addObserver( + cookieNotExpectedObserver, + "http-on-modify-request" + ); + await NormandyApi.get(serverUrl); + await cookieNotExpectedDeferred.promise; + } +); diff --git a/toolkit/components/normandy/test/unit/test_PrefUtils.js b/toolkit/components/normandy/test/unit/test_PrefUtils.js new file mode 100644 index 0000000000..57130d8783 --- /dev/null +++ b/toolkit/components/normandy/test/unit/test_PrefUtils.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PrefUtils } = ChromeUtils.importESModule( + "resource://normandy/lib/PrefUtils.sys.mjs" +); + +add_task(function getPrefGetsValues() { + const defaultBranch = Services.prefs.getDefaultBranch(""); + const userBranch = Services.prefs; + + defaultBranch.setBoolPref("test.bool", false); + userBranch.setBoolPref("test.bool", true); + defaultBranch.setIntPref("test.int", 1); + userBranch.setIntPref("test.int", 2); + defaultBranch.setStringPref("test.string", "default"); + userBranch.setStringPref("test.string", "user"); + + equal( + PrefUtils.getPref("test.bool", { branch: "user" }), + true, + "should read user branch bools" + ); + equal( + PrefUtils.getPref("test.int", { branch: "user" }), + 2, + "should read user branch ints" + ); + equal( + PrefUtils.getPref("test.string", { branch: "user" }), + "user", + "should read user branch strings" + ); + + equal( + PrefUtils.getPref("test.bool", { branch: "default" }), + false, + "should read default branch bools" + ); + equal( + PrefUtils.getPref("test.int", { branch: "default" }), + 1, + "should read default branch ints" + ); + equal( + PrefUtils.getPref("test.string", { branch: "default" }), + "default", + "should read default branch strings" + ); + + equal( + PrefUtils.getPref("test.bool"), + true, + "should read bools from the user branch by default" + ); + equal( + PrefUtils.getPref("test.int"), + 2, + "should read ints from the user branch by default" + ); + equal( + PrefUtils.getPref("test.string"), + "user", + "should read strings from the user branch by default" + ); + + equal( + PrefUtils.getPref("test.does_not_exist"), + null, + "Should return null for non-existent prefs by default" + ); + let defaultValue = Symbol(); + equal( + PrefUtils.getPref("test.does_not_exist", { defaultValue }), + defaultValue, + "Should use the passed default value" + ); +}); + +// This is an important test because the pref system can behave in strange ways +// when the user branch has a value, but the default branch does not. +add_task(function getPrefHandlesUserValueNoDefaultValue() { + Services.prefs.setStringPref("test.only-user-value", "user"); + + let defaultValue = Symbol(); + equal( + PrefUtils.getPref("test.only-user-value", { + branch: "default", + defaultValue, + }), + defaultValue + ); + equal(PrefUtils.getPref("test.only-user-value", { branch: "default" }), null); + equal(PrefUtils.getPref("test.only-user-value", { branch: "user" }), "user"); + equal(PrefUtils.getPref("test.only-user-value"), "user"); +}); + +add_task(function getPrefInvalidBranch() { + Assert.throws( + () => PrefUtils.getPref("test.pref", { branch: "invalid" }), + PrefUtils.UnexpectedPreferenceBranch + ); +}); + +add_task(function setPrefSetsValues() { + const defaultBranch = Services.prefs.getDefaultBranch(""); + const userBranch = Services.prefs; + + defaultBranch.setIntPref("test.int", 1); + userBranch.setIntPref("test.int", 2); + defaultBranch.setStringPref("test.string", "default"); + userBranch.setStringPref("test.string", "user"); + defaultBranch.setBoolPref("test.bool", false); + userBranch.setBoolPref("test.bool", true); + + PrefUtils.setPref("test.int", 3); + equal( + userBranch.getIntPref("test.int"), + 3, + "the user branch should change for ints" + ); + PrefUtils.setPref("test.int", 4, { branch: "default" }); + equal( + userBranch.getIntPref("test.int"), + 3, + "changing the default branch shouldn't affect the user branch for ints" + ); + PrefUtils.setPref("test.int", null, { branch: "user" }); + equal( + userBranch.getIntPref("test.int"), + 4, + "clearing the user branch should reveal the default value for ints" + ); + + PrefUtils.setPref("test.string", "user override"); + equal( + userBranch.getStringPref("test.string"), + "user override", + "the user branch should change for strings" + ); + PrefUtils.setPref("test.string", "default override", { branch: "default" }); + equal( + userBranch.getStringPref("test.string"), + "user override", + "changing the default branch shouldn't affect the user branch for strings" + ); + PrefUtils.setPref("test.string", null, { branch: "user" }); + equal( + userBranch.getStringPref("test.string"), + "default override", + "clearing the user branch should reveal the default value for strings" + ); + + PrefUtils.setPref("test.bool", false); + equal( + userBranch.getBoolPref("test.bool"), + false, + "the user branch should change for bools" + ); + // The above effectively unsets the user branch, since it is now the same as the default branch + PrefUtils.setPref("test.bool", true, { branch: "default" }); + equal( + userBranch.getBoolPref("test.bool"), + true, + "the default branch should change for bools" + ); + + defaultBranch.setBoolPref("test.bool", false); + userBranch.setBoolPref("test.bool", true); + equal( + userBranch.getBoolPref("test.bool"), + true, + "the precondition should hold" + ); + PrefUtils.setPref("test.bool", null, { branch: "user" }); + equal( + userBranch.getBoolPref("test.bool"), + false, + "setting the user branch to null should reveal the default value for bools" + ); +}); + +add_task(function setPrefInvalidBranch() { + Assert.throws( + () => PrefUtils.setPref("test.pref", "value", { branch: "invalid" }), + PrefUtils.UnexpectedPreferenceBranch + ); +}); + +add_task(function clearPrefClearsValues() { + const defaultBranch = Services.prefs.getDefaultBranch(""); + const userBranch = Services.prefs; + + defaultBranch.setStringPref("test.string", "default"); + userBranch.setStringPref("test.string", "user"); + equal( + userBranch.getStringPref("test.string"), + "user", + "the precondition should hold" + ); + PrefUtils.clearPref("test.string"); + equal( + userBranch.getStringPref("test.string"), + "default", + "clearing the user branch should reveal the default value for bools" + ); + + PrefUtils.clearPref("test.string", { branch: "default" }); + equal( + userBranch.getStringPref("test.string"), + "default", + "clearing the default branch shouldn't do anything" + ); +}); + +add_task(function clearPrefInvalidBranch() { + Assert.throws( + () => PrefUtils.clearPref("test.pref", { branch: "invalid" }), + PrefUtils.UnexpectedPreferenceBranch + ); +}); diff --git a/toolkit/components/normandy/test/unit/test_RecipeRunner.js b/toolkit/components/normandy/test/unit/test_RecipeRunner.js new file mode 100644 index 0000000000..710ac4d507 --- /dev/null +++ b/toolkit/components/normandy/test/unit/test_RecipeRunner.js @@ -0,0 +1,34 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const { updateAppInfo } = ChromeUtils.importESModule( + "resource://testing-common/AppInfo.sys.mjs" +); +const { RecipeRunner } = ChromeUtils.importESModule( + "resource://normandy/lib/RecipeRunner.sys.mjs" +); + +// Test that new build IDs trigger immediate recipe runs +add_task(async () => { + updateAppInfo({ + appBuildID: "new-build-id", + lastAppBuildID: "old-build-id", + }); + const runStub = sinon.stub(RecipeRunner, "run"); + const registerTimerStub = sinon.stub(RecipeRunner, "registerTimer"); + sinon.stub(RecipeRunner, "watchPrefs"); + + Services.prefs.setBoolPref("app.normandy.first_run", false); + + await RecipeRunner.init(); + Assert.deepEqual( + runStub.args, + [[{ trigger: "newBuildID" }]], + "RecipeRunner.run is called immediately on a new build ID" + ); + ok(registerTimerStub.called, "RecipeRunner.registerTimer registers a timer"); + + sinon.restore(); +}); diff --git a/toolkit/components/normandy/test/unit/test_addon_unenroll.js b/toolkit/components/normandy/test/unit/test_addon_unenroll.js new file mode 100644 index 0000000000..98750fc976 --- /dev/null +++ b/toolkit/components/normandy/test/unit/test_addon_unenroll.js @@ -0,0 +1,310 @@ +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); +const { BranchedAddonStudyAction } = ChromeUtils.importESModule( + "resource://normandy/actions/BranchedAddonStudyAction.sys.mjs" +); +const { BaseAction } = ChromeUtils.importESModule( + "resource://normandy/actions/BaseAction.sys.mjs" +); +const { TelemetryEvents } = ChromeUtils.importESModule( + "resource://normandy/lib/TelemetryEvents.sys.mjs" +); +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +const { AddonStudies } = ChromeUtils.importESModule( + "resource://normandy/lib/AddonStudies.sys.mjs" +); +const { PromiseUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromiseUtils.sys.mjs" +); + +/* import-globals-from utils.js */ +load("utils.js"); + +NormandyTestUtils.init({ add_task }); +const { decorate_task } = NormandyTestUtils; + +const global = this; + +add_task(async () => { + ExtensionTestUtils.init(global); + AddonTestUtils.init(global); + AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "1", + "1.9.2" + ); + AddonTestUtils.overrideCertDB(); + await AddonTestUtils.promiseStartupManager(); + + TelemetryEvents.init(); +}); + +decorate_task( + withMockApiServer(), + AddonStudies.withStudies([]), + async function test_addon_unenroll({ server: apiServer }) { + const ID = "study@tests.mozilla.org"; + + // Create a test extension that uses webextension experiments to install + // an unenroll listener. + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "1.0", + + browser_specific_settings: { + gecko: { id: ID }, + }, + + experiment_apis: { + study: { + schema: "schema.json", + parent: { + scopes: ["addon_parent"], + script: "api.js", + paths: [["study"]], + }, + }, + }, + }, + + files: { + "schema.json": JSON.stringify([ + { + namespace: "study", + events: [ + { + name: "onStudyEnded", + type: "function", + }, + ], + }, + ]), + + "api.js": () => { + // The code below is serialized into a file embedded in an extension. + // But by including it here as code, eslint can analyze it. However, + // this code runs in a different environment with different globals, + // the following two lines avoid false eslint warnings: + /* globals browser, ExtensionAPI */ + /* eslint-disable-next-line no-shadow */ + const { AddonStudies } = ChromeUtils.importESModule( + "resource://normandy/lib/AddonStudies.sys.mjs" + ); + const { ExtensionCommon } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionCommon.sys.mjs" + ); + this.study = class extends ExtensionAPI { + getAPI(context) { + return { + study: { + onStudyEnded: new ExtensionCommon.EventManager({ + context, + name: "study.onStudyEnded", + register: fire => { + AddonStudies.addUnenrollListener( + this.extension.id, + reason => fire.sync(reason) + ); + return () => {}; + }, + }).api(), + }, + }; + } + }; + }, + }, + + background() { + browser.study.onStudyEnded.addListener(reason => { + browser.test.sendMessage("got-event", reason); + return new Promise(resolve => { + browser.test.onMessage.addListener(resolve); + }); + }); + }, + }); + + const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + server.registerFile("/study.xpi", xpi); + + const API_ID = 999; + apiServer.registerPathHandler( + `/api/v1/extension/${API_ID}/`, + (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write( + JSON.stringify({ + id: API_ID, + name: "Addon Unenroll Fixture", + xpi: "http://example.com/study.xpi", + extension_id: ID, + version: "1.0", + hash: CryptoUtils.getFileHash(xpi, "sha256"), + hash_algorithm: "sha256", + }) + ); + } + ); + + // Begin by telling Normandy to install the test extension above + // that uses a webextension experiment to register a blocking callback + // to be invoked when the study ends. + let extension = ExtensionTestUtils.expectExtension(ID); + + const RECIPE_ID = 1; + const UNENROLL_REASON = "test-ending"; + let action = new BranchedAddonStudyAction(); + await action.processRecipe( + { + id: RECIPE_ID, + type: "addon-study", + arguments: { + slug: "addon-unenroll-test", + userFacingDescription: "A recipe to test add-on unenrollment", + userFacingName: "Add-on Unenroll Test", + isEnrollmentPaused: false, + branches: [ + { + ratio: 1, + slug: "only", + extensionApiId: API_ID, + }, + ], + }, + }, + BaseAction.suitability.FILTER_MATCH + ); + + await extension.awaitStartup(); + + let addon = await AddonManager.getAddonByID(ID); + ok(addon, "Extension is installed"); + + // Tell Normandy to end the study, the extension event should be fired. + let unenrollPromise = action.unenroll(RECIPE_ID, UNENROLL_REASON); + + let receivedReason = await extension.awaitMessage("got-event"); + info("Got onStudyEnded event in extension"); + equal(receivedReason, UNENROLL_REASON, "Unenroll reason should be passed"); + + // The extension has not yet finished its unenrollment tasks, so it + // should not yet be uninstalled. + addon = await AddonManager.getAddonByID(ID); + ok(addon, "Extension has not yet been uninstalled"); + + // Once the extension does resolve the promise returned from the + // event listener, the uninstall can proceed. + extension.sendMessage("resolve"); + await unenrollPromise; + + addon = await AddonManager.getAddonByID(ID); + equal( + addon, + null, + "After resolving studyEnded promise, extension is uninstalled" + ); + } +); + +/* Test that a broken unenroll listener doesn't stop the add-on from being removed */ +decorate_task( + withMockApiServer(), + AddonStudies.withStudies([]), + async function test_addon_unenroll({ server: apiServer }) { + const ID = "study@tests.mozilla.org"; + + // Create a dummy webextension + // an unenroll listener that throws an error. + let xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + version: "1.0", + + browser_specific_settings: { + gecko: { id: ID }, + }, + }, + }); + + const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] }); + server.registerFile("/study.xpi", xpi); + + const API_ID = 999; + apiServer.registerPathHandler( + `/api/v1/extension/${API_ID}/`, + (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write( + JSON.stringify({ + id: API_ID, + name: "Addon Fixture", + xpi: "http://example.com/study.xpi", + extension_id: ID, + version: "1.0", + hash: CryptoUtils.getFileHash(xpi, "sha256"), + hash_algorithm: "sha256", + }) + ); + } + ); + + // Begin by telling Normandy to install the test extension above that uses a + // webextension experiment to register a callback when the study ends that + // throws an error. + let extension = ExtensionTestUtils.expectExtension(ID); + + const RECIPE_ID = 1; + const UNENROLL_REASON = "test-ending"; + let action = new BranchedAddonStudyAction(); + await action.processRecipe( + { + id: RECIPE_ID, + type: "addon-study", + arguments: { + slug: "addon-unenroll-test", + userFacingDescription: "A recipe to test add-on unenrollment", + userFacingName: "Add-on Unenroll Test", + isEnrollmentPaused: false, + branches: [ + { + ratio: 1, + slug: "only", + extensionApiId: API_ID, + }, + ], + }, + }, + BaseAction.suitability.FILTER_MATCH + ); + + await extension.startupPromise; + + let addon = await AddonManager.getAddonByID(ID); + ok(addon, "Extension is installed"); + + let listenerDeferred = PromiseUtils.defer(); + + AddonStudies.addUnenrollListener(ID, () => { + listenerDeferred.resolve(); + throw new Error("This listener is busted"); + }); + + // Tell Normandy to end the study, the extension event should be fired. + await action.unenroll(RECIPE_ID, UNENROLL_REASON); + await listenerDeferred; + + addon = await AddonManager.getAddonByID(ID); + equal( + addon, + null, + "Extension is uninstalled even though it threw an exception in the callback" + ); + } +); diff --git a/toolkit/components/normandy/test/unit/utils.js b/toolkit/components/normandy/test/unit/utils.js new file mode 100644 index 0000000000..cffe634c91 --- /dev/null +++ b/toolkit/components/normandy/test/unit/utils.js @@ -0,0 +1,135 @@ +"use strict"; +/* eslint-disable no-unused-vars */ + +// Loaded into the same scope as head_xpc.js +/* import-globals-from head_xpc.js */ + +const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" +); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const { NormandyApi } = ChromeUtils.importESModule( + "resource://normandy/lib/NormandyApi.sys.mjs" +); +const { NormandyTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/NormandyTestUtils.sys.mjs" +); + +const CryptoHash = Components.Constructor( + "@mozilla.org/security/hash;1", + "nsICryptoHash", + "initWithString" +); +const FileInputStream = Components.Constructor( + "@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init" +); + +class MockResponse { + constructor(content) { + this.content = content; + } + + async text() { + return this.content; + } + + async json() { + return JSON.parse(this.content); + } +} + +function withServer(server) { + return function (testFunction) { + return NormandyTestUtils.decorate( + NormandyTestUtils.withMockPreferences(), + async function inner({ mockPreferences, ...args }) { + const serverUrl = `http://localhost:${server.identity.primaryPort}`; + mockPreferences.set("app.normandy.api_url", `${serverUrl}/api/v1`); + NormandyApi.clearIndexCache(); + + try { + await testFunction({ ...args, serverUrl, mockPreferences, server }); + } finally { + await new Promise(resolve => server.stop(resolve)); + } + } + ); + }; +} + +function makeScriptServer(scriptPath) { + const server = new HttpServer(); + server.registerContentType("sjs", "sjs"); + server.registerFile("/", do_get_file(scriptPath)); + server.start(-1); + return server; +} + +function withScriptServer(scriptPath) { + return withServer(makeScriptServer(scriptPath)); +} + +function makeMockApiServer(directory) { + const server = new HttpServer(); + server.registerDirectory("/", directory); + + server.setIndexHandler(async function (request, response) { + response.processAsync(); + const dir = request.getProperty("directory"); + const index = dir.clone(); + index.append("index.json"); + + if (!index.exists()) { + response.setStatusLine("1.1", 404, "Not Found"); + response.write(`Cannot find path ${index.path}`); + response.finish(); + return; + } + + try { + const contents = await IOUtils.readUTF8(index.path); + response.write(contents); + } catch (e) { + response.setStatusLine("1.1", 500, "Server error"); + response.write(e.toString()); + } finally { + response.finish(); + } + }); + + server.start(-1); + return server; +} + +function withMockApiServer(apiName = "mock_api") { + return withServer(makeMockApiServer(do_get_file(apiName))); +} + +const CryptoUtils = { + _getHashStringForCrypto(aCrypto) { + // return the two-digit hexadecimal code for a byte + let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2); + + // convert the binary hash data to a hex string. + let binary = aCrypto.finish(false); + let hash = Array.from(binary, c => toHexString(c.charCodeAt(0))); + return hash.join("").toLowerCase(); + }, + + /** + * Get the computed hash for a given file + * @param {nsIFile} file The file to be hashed + * @param {string} [algorithm] The hashing algorithm to use + */ + getFileHash(file, algorithm = "sha256") { + const crypto = CryptoHash(algorithm); + const fis = new FileInputStream(file, -1, -1, false); + crypto.updateFromStream(fis, file.fileSize); + const hash = this._getHashStringForCrypto(crypto); + fis.close(); + return hash; + }, +}; diff --git a/toolkit/components/normandy/test/unit/xpcshell.ini b/toolkit/components/normandy/test/unit/xpcshell.ini new file mode 100644 index 0000000000..e2ec476ce9 --- /dev/null +++ b/toolkit/components/normandy/test/unit/xpcshell.ini @@ -0,0 +1,17 @@ +[DEFAULT] +head = head_xpc.js +firefox-appdir = browser +support-files = + mock_api/** + invalid_recipe_signature_api/** + query_server.sjs + echo_server.sjs + cookie_server.sjs + utils.js +tags = normandy + +[test_Normandy.js] +[test_PrefUtils.js] +[test_addon_unenroll.js] +[test_NormandyApi.js] +[test_RecipeRunner.js] |