Contexte
Récemment, nous avons travaillé sur les tests d’une application {shiny}
qui repose sur des valeurs stockées dans l’objet session$request
. Cet objet est un environnement qui capture les détails de l’échange HTTP entre R et le navigateur. Sans trop entrer dans les détails techniques (même si cela me tente 😅), il est important de comprendre que session$request
contient des informations fournies à la fois par le navigateur et par tout proxy redirigeant les requêtes.
Notre application est déployée derrière un proxy dans un environnement Microsoft Azure. Ici, le service d’authentification ajoute plusieurs en-têtes pour valider l’identité des utilisateurs (voir la documentation pour plus de détails). Des en-têtes comme X-MS-CLIENT-PRINCIPAL
et X-MS-CLIENT-PRINCIPAL-ID
sont essentiels pour identifier les utilisateurs, et l’application {shiny}
en dépend pour gérer l’authentification.
Tester les en-têtes
Lorsque qu’un utilisateur se connecte à l’application, ses identifiants sont extraits d’un en-tête et stockés pour être utilisés dans toute l’application. Voici un exemple simplifié de ce fonctionnement :
library(shiny)
ui <- fluidPage(
textOutput("user_id")
)
server <- function(input, output, session) {
r <- reactiveValues(
email = NULL
)
observe({
r$email <- session$request$HTTP_X_MS_CLIENT_PRINCIPAL_NAME
})
output$user_id <- renderText({
req(r$email)
sprintf("Bonjour %s", r$email)
})
}
shinyApp(ui, server)
Tester cette fonctionnalité, notamment dans des environnements d’intégration continue (CI), peut être un défi.
Dans notre cas, nous aimerions avoir quelque chose comme ceci :
test_that("app server", {
# Ajuster la session ici
testServer(app_server, {
# Attendre que la session soit initialisée
session$elapse(1)
expect_equal(
r$email,
"[email protected]"
)
})
})
Cependant, les en-têtes d’authentification comme HTTP_X_MS_CLIENT_PRINCIPAL_NAME
sont absents lors des tests automatisés (notamment sur le CI), nous devons donc trouver un moyen de simuler leur présence. Le package {shiny}
fournit la classe MockShinySession
pour les tests, mais elle ne simule pas nativement un objet session$request
réaliste. Explorons comment contourner cette limitation.
Redéfinir session$request
Nous avons d’abord essayé de modifier directement session$request
, mais cela ne fonctionne pas :
> session <- MockShinySession$new()
> session$request
<environment: 0x13a032600>
Warning message:
In (function (value) :
session$request doesn't currently simulate a realistic request on MockShinySession
Peut-être pouvons-nous ajouter une nouvelle entrée ?
> session$request$HTTP_X_MS_CLIENT_PRINCIPAL_NAME <- "test"
Error in (function (value) : session$request can't be assigned to
In addition: Warning message:
In (function (value) :
session$request doesn't currently simulate a realistic request on MockShinySession
Aïe, ça ne fonctionne pas. Continuons à explorer. Qu’est-ce que session
?
> class(session)
[1] "MockShinySession" "R6"
> class(session$request)
[1] "environment"
Comme on peut le voir, c’est un objet R6, une instance de la classe MockShinySession
, et session$request
est un environnement. Nous voulons accéder, dans notre application, à session$request$HTTP_X_MS_CLIENT_PRINCIPAL_NAME
. Peut-être pourrions-nous redéfinir request
?
request
se trouve dans le champ active
de la classe R6 :
> MockShinySession$active
# [...]
$request
function (value)
{
if (!missing(value)) {
stop("session$request can't be assigned to")
}
warning("session$request doesn't currently simulate a realistic request on MockShinySession")
new.env(parent = emptyenv())
}
Pour redéfinir l’objet request
, nous pouvons utiliser la méthode set()
de la classe R6. Voici comment redéfinir le comportement :
MockShinySession$set(
"active",
"request",
function(value) {
return(
list(
"HTTP_X_MS_CLIENT_PRINCIPAL_NAME" = "[email protected]"
)
)
},
overwrite = TRUE
)
Désormais, la session fonctionne comme attendu :
> session <- MockShinySession$new()
> session$request
$HTTP_X_MS_CLIENT_PRINCIPAL_NAME
[1] "[email protected]"
Écrire le test
Avec le request
redéfini, nous pouvons maintenant écrire un test fonctionnel :
test_that("app server", {
MockShinySession$set(
"active",
"request",
function(value) {
return(
list(
"HTTP_X_MS_CLIENT_PRINCIPAL_NAME" = "[email protected]"
)
)
},
overwrite = TRUE
)
testServer(app_server, {
session$elapse(1)
expect_equal(
r$email,
"[email protected]"
)
})
})
Nettoyage après les tests
Une dernière chose : nous devons nettoyer notre test pour que l’objet session reste inchangé après son exécution. Pour cela, nous utilisons on.exit
pour restaurer l’ancien comportement :
test_that("app server", {
old_request <- MockShinySession$active$request
on.exit({
MockShinySession$set(
"active",
"request",
old_request,
overwrite = TRUE
)
})
MockShinySession$set(
"active",
"request",
function(value) {
return(
list(
"HTTP_X_MS_CLIENT_PRINCIPAL_NAME" = "[email protected]"
)
)
},
overwrite = TRUE
)
testServer(app_server, {
session$elapse(1)
expect_equal(
r$email,
"[email protected]"
)
})
})
Cette configuration garantit que nos tests restent isolés et fiables, même dans des environnements CI. En tirant parti de la flexibilité des objets R6, nous pouvons contrôler et simuler entièrement session$request
pour tester les logiques dépendantes de l’authentification.