mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 22:48:11 +02:00
Lot 12 : notifications Web Push tenues par la façade (LOKI_PUSH=rust)
La façade sert la clé VAPID et les (dés)abonnements, et pousse elle-même les notifications « réponse prête » quand LOKI_PUSH=rust (expérimental ; le Go reste le défaut) : chiffrement RFC 8291 (aes128gcm, enregistrement complété à 4096 octets comme webpush-go), signature VAPID RFC 8292 (JWT ES256), abonnements purgés sur 404/410, par ring déjà présent via rustls. Mêmes clés en base que le Go. Côté Go : facade_push.go remet les routes du tunnel à la façade, et sendPushToAll (fin de tour, fin de tâche) lui délègue l'envoi. Parité : parity_push_test.go tire du Go les durées et onze appels de route avec la liste des abonnements en base, rejoués par tests/push.rs ; le chiffrement reproduit l'exemple de la RFC 8291 à l'octet près, et un envoi de bout en bout est déchiffré côté navigateur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
20 files changed
+1320
-5
No files matched your search
@@ -0,0 +1,109 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// parity_push_test.go — journal de référence des notifications Web Push (lot
|
||||
// 12 de la migration Rust) : clé VAPID (générée une fois, stable), abonnement,
|
||||
// désabonnement, et le texte du corps de notification. Rangé dans
|
||||
// rust/loki-rs/tests/parity_push/push.json ; tests/push.rs le rejoue par la
|
||||
// façade. Les clés tirées au hasard sont masquées (seule leur forme compte).
|
||||
//
|
||||
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteNotifications
|
||||
|
||||
const parityPushFile = "../../rust/loki-rs/tests/parity_push/push.json"
|
||||
|
||||
type parityPushCall struct {
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Status int `json:"status"`
|
||||
Out any `json:"out"`
|
||||
// Après l'appel : les abonnements enregistrés (JSON brut, tel qu'en base).
|
||||
Subs string `json:"subs"`
|
||||
}
|
||||
|
||||
type parityPushFixture struct {
|
||||
Durations map[string]string `json:"durations"`
|
||||
Calls []parityPushCall `json:"calls"`
|
||||
}
|
||||
|
||||
func TestPariteNotifications(t *testing.T) {
|
||||
testHome(t)
|
||||
var fx parityPushFixture
|
||||
fx.Durations = map[string]string{}
|
||||
for _, ms := range []int64{0, 400, 600, 42_000, 185_000, 3_600_000, 4_330_000, -5_000} {
|
||||
fx.Durations[strings.TrimSpace(time.Duration(ms*int64(time.Millisecond)).String())] = fmtDurFR(time.Duration(ms) * time.Millisecond)
|
||||
}
|
||||
keyRe := regexp.MustCompile(`"key":"[A-Za-z0-9_-]{87}"`)
|
||||
call := func(method, path, body string) parityPushCall {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
switch path {
|
||||
case "/api/push/key":
|
||||
handlePushKey(w, r)
|
||||
case "/api/push/subscribe":
|
||||
handlePushSubscribe(w, r)
|
||||
case "/api/push/unsubscribe":
|
||||
handlePushUnsubscribe(w, r)
|
||||
default:
|
||||
t.Fatalf("route inattendue %s", path)
|
||||
}
|
||||
out := keyRe.ReplaceAllString(w.Body.String(), `"key":"{{CLE}}"`)
|
||||
c := parityPushCall{Method: method, Path: path, Body: body, Status: w.Code, Subs: string(getBytes(bkState, stPushSubs))}
|
||||
if err := json.Unmarshal([]byte(out), &c.Out); err != nil {
|
||||
t.Fatalf("%s : %v (%s)", path, err, w.Body.String())
|
||||
}
|
||||
return c
|
||||
}
|
||||
for _, c := range []struct{ m, p, b string }{
|
||||
{"GET", "/api/push/key", ""},
|
||||
{"POST", "/api/push/subscribe", `{"endpoint":"https://push.exemple.fr/a","keys":{"p256dh":"BAAA","auth":"x1"}}`},
|
||||
{"POST", "/api/push/subscribe", `{"endpoint":"https://push.exemple.fr/b","keys":{"p256dh":"BBBB","auth":"x2"},"expirationTime":null}`},
|
||||
{"POST", "/api/push/subscribe", `{"endpoint":"https://push.exemple.fr/a","keys":{"p256dh":"BCCC","auth":"x3"}}`},
|
||||
{"POST", "/api/push/subscribe", `{"keys":{"p256dh":"B","auth":"y"}}`},
|
||||
{"POST", "/api/push/subscribe", `pas du json`},
|
||||
{"POST", "/api/push/unsubscribe", `{"endpoint":"https://push.exemple.fr/inconnu"}`},
|
||||
{"POST", "/api/push/unsubscribe", `{"endpoint":"https://push.exemple.fr/a"}`},
|
||||
{"POST", "/api/push/unsubscribe", `pas du json`},
|
||||
{"POST", "/api/push/unsubscribe", `{"endpoint":"https://push.exemple.fr/b"}`},
|
||||
{"GET", "/api/push/key", ""},
|
||||
} {
|
||||
fx.Calls = append(fx.Calls, call(c.m, c.p, c.b))
|
||||
}
|
||||
// La paire VAPID est stable : la clé servie est la clé rangée.
|
||||
if pub := getStr(bkState, stPushVAPIDPub); len(pub) != 87 || len(getStr(bkState, stPushVAPIDPriv)) != 43 {
|
||||
t.Fatalf("paire VAPID inattendue : %q", pub)
|
||||
}
|
||||
|
||||
got, err := parityJSON(fx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
||||
if err := os.MkdirAll(filepath.Dir(parityPushFile), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(parityPushFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(parityPushFile)
|
||||
if err != nil {
|
||||
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Errorf("les notifications ne produisent plus le journal rangé dans %s.\n"+
|
||||
"Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteNotifications,\n"+
|
||||
"puis cargo test --test push dit si le port Rust suit.\n--- obtenu ---\n%s", parityPushFile, got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user