diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/normandy/test/unit | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/normandy/test/unit')
18 files changed, 1165 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..0890c00afe --- /dev/null +++ b/toolkit/components/normandy/test/unit/head_xpc.js @@ -0,0 +1,4 @@ +"use strict"; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); 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..fc729fc637 --- /dev/null +++ b/toolkit/components/normandy/test/unit/invalid_recipe_signature_api/api/v1/recipe/signed/index.json @@ -0,0 +1 @@ +[{"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..ee5d8be9e1 --- /dev/null +++ b/toolkit/components/normandy/test/unit/mock_api/api/v1/recipe/signed/index.json @@ -0,0 +1 @@ +[{"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..97d47ee309 --- /dev/null +++ b/toolkit/components/normandy/test/unit/query_server.sjs @@ -0,0 +1,30 @@ +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_NormandyApi.js b/toolkit/components/normandy/test/unit/test_NormandyApi.js new file mode 100644 index 0000000000..781eafcef5 --- /dev/null +++ b/toolkit/components/normandy/test/unit/test_NormandyApi.js @@ -0,0 +1,276 @@ +/* globals sinon */ +"use strict"; + +ChromeUtils.import("resource://gre/modules/CanonicalJSON.jsm", this); +ChromeUtils.import("resource://gre/modules/osfile.jsm", this); +ChromeUtils.import("resource://normandy/lib/NormandyApi.jsm", this); +ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm", this); + +Cu.importGlobalProperties(["fetch"]); + +load( + "utils.js" +); /* globals withMockApiServer, MockResponse, withScriptServer, withServer, makeMockApiServer */ + +add_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" + ); + }) +); + +add_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" + ); + }) +); + +add_task( + withMockApiServer(async function test_getApiUrlSlashes( + serverUrl, + preferences + ) { + const fakeResponse = new MockResponse( + JSON.stringify({ "test-endpoint": `${serverUrl}/test/` }) + ); + const mockGet = sinon + .stub(NormandyApi, "get") + .callsFake(async () => fakeResponse); + + // without slash + { + NormandyApi.clearIndexCache(); + preferences.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(); + preferences.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). +add_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). +const invalidSignatureServer = makeMockApiServer( + do_get_file("invalid_recipe_signature_api") +); +add_task( + withServer( + invalidSignatureServer, + 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"); + } + } + ) +); + +add_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"), + }); + }) +); + +add_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", + }); + }) +); + +add_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." + ); + }) +); + +add_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. +add_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_RecipeRunner.js b/toolkit/components/normandy/test/unit/test_RecipeRunner.js new file mode 100644 index 0000000000..38b8cce8ce --- /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.import( + "resource://testing-common/AppInfo.jsm" +); +const { RecipeRunner } = ChromeUtils.import( + "resource://normandy/lib/RecipeRunner.jsm" +); + +// 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..ef32225ae1 --- /dev/null +++ b/toolkit/components/normandy/test/unit/test_addon_unenroll.js @@ -0,0 +1,322 @@ +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); +const { ExtensionTestUtils } = ChromeUtils.import( + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); +const { BranchedAddonStudyAction } = ChromeUtils.import( + "resource://normandy/actions/BranchedAddonStudyAction.jsm" +); +const { BaseAction } = ChromeUtils.import( + "resource://normandy/actions/BaseAction.jsm" +); +const { TelemetryEvents } = ChromeUtils.import( + "resource://normandy/lib/TelemetryEvents.jsm" +); +const { AddonManager } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" +); +const { NormandyTestUtils } = ChromeUtils.import( + "resource://testing-common/NormandyTestUtils.jsm" +); +const { AddonStudies } = ChromeUtils.import( + "resource://normandy/lib/AddonStudies.jsm" +); +const { PromiseUtils } = ChromeUtils.import( + "resource://gre/modules/PromiseUtils.jsm" +); + +NormandyTestUtils.init({ add_task }); +const { decorate_task } = NormandyTestUtils; + +const global = this; + +load("utils.js"); /* globals withMockApiServer, CryptoUtils */ + +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( + _serverUrl, + _preferences, + apiServer, + _studies + ) { + 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", + + applications: { + 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.import( + "resource://normandy/lib/AddonStudies.jsm" + ); + const { ExtensionCommon } = ChromeUtils.import( + "resource://gre/modules/ExtensionCommon.jsm" + ); + 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( + _serverUrl, + _preferences, + apiServer, + _studies + ) { + 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", + + applications: { + 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..3af9d1f86f --- /dev/null +++ b/toolkit/components/normandy/test/unit/utils.js @@ -0,0 +1,181 @@ +"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.import( + "resource://gre/modules/Preferences.jsm" +); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +ChromeUtils.import("resource://gre/modules/osfile.jsm", this); +ChromeUtils.import("resource://normandy/lib/NormandyApi.jsm", this); + +const CryptoHash = Components.Constructor( + "@mozilla.org/security/hash;1", + "nsICryptoHash", + "initWithString" +); +const FileInputStream = Components.Constructor( + "@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init" +); + +const preferenceBranches = { + user: Preferences, + default: new Preferences({ defaultBranch: true }), +}; + +// duplicated from test/browser/head.js until we move everything over to mochitests. +function withMockPreferences(testFunction) { + return async function inner(...args) { + const prefManager = new MockPreferences(); + try { + await testFunction(...args, prefManager); + } finally { + prefManager.cleanup(); + } + }; +} + +class MockPreferences { + constructor() { + this.oldValues = { user: {}, default: {} }; + } + + set(name, value, branch = "user") { + this.preserve(name, branch); + preferenceBranches[branch].set(name, value); + } + + preserve(name, branch) { + if (!(name in this.oldValues[branch])) { + this.oldValues[branch][name] = preferenceBranches[branch].get( + name, + undefined + ); + } + } + + cleanup() { + for (const [branchName, values] of Object.entries(this.oldValues)) { + const preferenceBranch = preferenceBranches[branchName]; + for (const [name, value] of Object.entries(values)) { + if (value !== undefined) { + preferenceBranch.set(name, value); + } else { + preferenceBranch.reset(name); + } + } + } + } +} + +class MockResponse { + constructor(content) { + this.content = content; + } + + async text() { + return this.content; + } + + async json() { + return JSON.parse(this.content); + } +} + +function withServer(server, task) { + return withMockPreferences(async function inner(preferences) { + const serverUrl = `http://localhost:${server.identity.primaryPort}`; + preferences.set("app.normandy.api_url", `${serverUrl}/api/v1`); + preferences.set( + "security.content.signature.root_hash", + // Hash of the key that signs the normandy dev certificates + "4C:35:B1:C3:E3:12:D9:55:E7:78:ED:D0:A7:E7:8A:38:83:04:EF:01:BF:FA:03:29:B2:46:9F:3C:C5:EC:36:04" + ); + NormandyApi.clearIndexCache(); + + try { + await task(serverUrl, preferences, 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, task) { + return withServer(makeScriptServer(scriptPath), task); +} + +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 OS.File.read(index.path, { encoding: "utf-8" }); + 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(task) { + return withServer(makeMockApiServer(do_get_file("mock_api")), task); +} + +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..ca2c9c65d7 --- /dev/null +++ b/toolkit/components/normandy/test/unit/xpcshell.ini @@ -0,0 +1,15 @@ +[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_addon_unenroll.js] +[test_NormandyApi.js] +[test_RecipeRunner.js] |