Skip to content
quarry Artifact [3fb8a5efac]
Login

Artifact [3fb8a5efac]

Artifact 3fb8a5efac766eee4b32945999c3889ed54dc0e5fefe2908587ba3e2f724bf73:


package api

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/ops"
)

// A browser refuses to register a worker served as anything but
// JavaScript, and a worker without Service-Worker-Allowed may only
// control the path it was served from — which would make a
// notification click unable to focus any page (RFC 0011 §6).
func TestServiceWorkerHeaders(t *testing.T) {
	rec := httptest.NewRecorder()
	ServiceWorker().ServeHTTP(rec, httptest.NewRequest("GET", "/sw.js", nil))
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d", rec.Code)
	}
	if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/javascript") {
		t.Errorf("Content-Type = %q", ct)
	}
	if got := rec.Header().Get("Service-Worker-Allowed"); got != "/" {
		t.Errorf("Service-Worker-Allowed = %q", got)
	}
	body := rec.Body.String()
	// The three jobs the worker exists to do. Asserted as the event
	// names it registers, not as copy.
	for _, want := range []string{"'push'", "'notificationclick'", "'pushsubscriptionchange'"} {
		if !strings.Contains(body, want) {
			t.Errorf("sw.js does not handle %s", want)
		}
	}
}

// The re-subscribe path must reach an endpoint that authenticates the
// way a service worker CAN. /api/* is HTTP Basic only (router.go), and
// a worker has no credentials to send — its fetch would be answered
// 401 and the device would go quiet with nobody told. So it posts to
// the cookie-session web routes with credentials: 'same-origin'.
func TestServiceWorkerReSubscribesThroughACookieAuthedRoute(t *testing.T) {
	body := serviceWorkerSource()
	for _, want := range []string{"'/me/push/key'", "'/me/push'", "credentials: 'same-origin'"} {
		if !strings.Contains(body, want) {
			t.Errorf("sw.js does not use %s", want)
		}
	}
	// The API routes cannot answer a worker's cookie fetch at all.
	if strings.Contains(body, "/api/v1/me/push") {
		t.Error("sw.js fetches an HTTP-Basic-only API route; a worker has no credentials for it")
	}
}

// A notification's url arrives from outside quarry — it is whatever the
// workflow's data carried. Opening or focusing it unchecked turns every
// push into an open redirect, so the worker compares ORIGINS with the
// URL parser rather than looking for a substring.
func TestServiceWorkerGuardsAgainstForeignOrigins(t *testing.T) {
	body := serviceWorkerSource()
	for _, want := range []string{"new URL(", "self.location.origin", "sameOrigin"} {
		if !strings.Contains(body, want) {
			t.Errorf("sw.js has no same-origin check: missing %s", want)
		}
	}
	// indexOf on a URL is the check this replaces: "https://evil.test/"
	// contains "/" and would match every client.
	if strings.Contains(body, ".indexOf(url)") {
		t.Error("sw.js still matches clients by substring")
	}
}

// serviceWorkerSource is what the route actually serves — asserted
// through the handler so a test cannot pass against a file the route
// does not ship.
func serviceWorkerSource() string {
	rec := httptest.NewRecorder()
	ServiceWorker().ServeHTTP(rec, httptest.NewRequest("GET", "/sw.js", nil))
	return rec.Body.String()
}

// Mounted by the API router too, so a node that serves this router at
// the root needs no extra wiring.
func TestRouterServesTheServiceWorker(t *testing.T) {
	h, err := Router(ops.NewRegistry(), "0.0.0-test")
	if err != nil {
		t.Fatal(err)
	}
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest("GET", "/sw.js", nil))
	if rec.Code != http.StatusOK || rec.Header().Get("Service-Worker-Allowed") != "/" {
		t.Fatalf("status = %d, headers = %+v", rec.Code, rec.Header())
	}
}