package endpoint import ( "errors" "testing" "github.com/wangjia/pangolin/infra/domains/tools/internal/notice" "github.com/wangjia/pangolin/infra/domains/tools/internal/sign" ) func buildSigned(t *testing.T, p Payload, keyID string, version uint64) ([]byte, sign.KeyRing) { t.Helper() pub, priv, err := sign.GenerateKey() if err != nil { t.Fatal(err) } env, err := Build(p, keyID, version, "2026-06-13T00:00:00Z", priv) if err != nil { t.Fatalf("Build: %v", err) } raw, err := sign.Marshal(env) if err != nil { t.Fatal(err) } return raw, sign.KeyRing{keyID: pub} } func validPayload() Payload { return Payload{ APIDomains: []string{"api-b.example.net", "api-a.example.com"}, MirrorURLs: []string{"https://m1.example.com/endpoints.v1.json"}, } } func TestBuildVerifyRoundTrip(t *testing.T) { raw, ring := buildSigned(t, validPayload(), "k1", 3) env, p, err := VerifyDocument(raw, ring, 0) if err != nil { t.Fatalf("VerifyDocument: %v", err) } if env.Version != 3 { t.Fatalf("version = %d", env.Version) } // Normalization should have sorted the domains. if p.APIDomains[0] != "api-a.example.com" { t.Fatalf("domains not normalized/sorted: %v", p.APIDomains) } } func TestRollbackRejected(t *testing.T) { raw, ring := buildSigned(t, validPayload(), "k1", 5) // Client already trusts version 5; a v5 (replay) or lower must be rejected. if _, _, err := VerifyDocument(raw, ring, 5); !errors.Is(err, ErrRollback) { t.Fatalf("want ErrRollback for equal version, got %v", err) } if _, _, err := VerifyDocument(raw, ring, 9); !errors.Is(err, ErrRollback) { t.Fatalf("want ErrRollback for lower version, got %v", err) } // A newer current baseline that is actually older than doc is accepted. if _, _, err := VerifyDocument(raw, ring, 4); err != nil { t.Fatalf("v5 doc over current=4 should pass, got %v", err) } } func TestTamperRejected(t *testing.T) { raw, ring := buildSigned(t, validPayload(), "k1", 1) // Flip a byte inside the JSON. tampered := make([]byte, len(raw)) copy(tampered, raw) for i := range tampered { if tampered[i] == 'a' { tampered[i] = 'b' break } } if _, _, err := VerifyDocument(tampered, ring, 0); err == nil { t.Fatal("tampered document accepted") } } func TestKeyRotationTransition(t *testing.T) { oldPub, _, _ := sign.GenerateKey() newPub, newPriv, _ := sign.GenerateKey() env, err := Build(validPayload(), "v2", 2, "2026-06-13T00:00:00Z", newPriv) if err != nil { t.Fatal(err) } raw, _ := sign.Marshal(env) // Rotation window: client carries both old and new public keys. ring := sign.KeyRing{"v1": oldPub, "v2": newPub} if _, _, err := VerifyDocument(raw, ring, 0); err != nil { t.Fatalf("rotation window verify failed: %v", err) } } func TestValidationRejectsEmptyDomains(t *testing.T) { pub, priv, _ := sign.GenerateKey() _ = pub if _, err := Build(Payload{MirrorURLs: []string{"https://m/x"}}, "k1", 1, "t", priv); err == nil { t.Fatal("want error for empty api_domains") } } func TestValidationRejectsBadMirrorURL(t *testing.T) { _, priv, _ := sign.GenerateKey() p := Payload{APIDomains: []string{"a.example.com"}, MirrorURLs: []string{"not-a-url"}} if _, err := Build(p, "k1", 1, "t", priv); err == nil { t.Fatal("want error for bad mirror url") } } func TestNoticeInPayloadValidated(t *testing.T) { _, priv, _ := sign.GenerateKey() p := validPayload() p.Notice = ¬ice.Notice{ID: "n1", Level: "bogus", TitleZH: "x", TitleEn: "x", PublishedAt: "t"} if _, err := Build(p, "k1", 1, "t", priv); err == nil { t.Fatal("want error for invalid notice level") } } func TestChannelPreserved(t *testing.T) { p := validPayload() p.Channel = "play-store" raw, ring := buildSigned(t, p, "k1", 1) _, got, err := VerifyDocument(raw, ring, 0) if err != nil { t.Fatal(err) } if got.Channel != "play-store" { t.Fatalf("channel lost: %q", got.Channel) } }