package ui import ( "crypto/ed25519" "crypto/rand" "encoding/base64" "reflect" "strings" "testing" "aegis/internal/cryptokit" "aegis/internal/identity" ) func TestNormalizeGroups(t *testing.T) { got, err := normalizeGroups(" comp.lang.go, sci.crypt,comp.lang.go ") if err != nil { t.Fatal(err) } want := []string{"comp.lang.go", "sci.crypt"} if !reflect.DeepEqual(got, want) { t.Fatalf("normalizeGroups() = %v, want %v", got, want) } } func TestNormalizeGroupsRejectsInjection(t *testing.T) { if _, err := normalizeGroups("comp.lang.go\r\nPOST"); err == nil { t.Fatal("normalizeGroups() accepted protocol injection") } } func TestFormatCount(t *testing.T) { for _, test := range []struct { value int64 want string }{{0, "0"}, {999, "999"}, {1000, "1 000"}, {1234567, "1 234 567"}} { if got := formatCount(test.value); got != test.want { t.Errorf("formatCount(%d) = %q, want %q", test.value, got, test.want) } } } func TestBuildTextArticleUsesMIMEAndEncodesUTF8Headers(t *testing.T) { article, err := buildTextArticle( []string{"alt.test"}, "Élodie ", "Caffè e privacy", "Corpo UTF-8: café\nseconda riga", ) if err != nil { t.Fatal(err) } for _, want := range []string{ "MIME-Version: 1.0\r\n", "Content-Type: text/plain; charset=UTF-8\r\n", "Content-Transfer-Encoding: 8bit\r\n", "Face: ", "Subject: =?UTF-8?q?Caff=C3=A8_e_privacy?=\r\n", "From: =?UTF-8?q?=C3=89lodie?= \r\n", "Corpo UTF-8: café\r\nseconda riga", } { if !strings.Contains(article, want) { t.Fatalf("article missing %q:\n%s", want, article) } } for _, line := range strings.Split(article, "\r\n") { if strings.Contains(line, "\n") { t.Fatalf("article contains bare LF: %q", line) } } } func TestBuildTextArticleRejectsInvalidBody(t *testing.T) { if _, err := buildTextArticle([]string{"alt.test"}, "reader@example.org", "Test", string([]byte{'x', 0xff})); err == nil { t.Fatal("buildTextArticle accepted invalid UTF-8") } } func TestBuildArticleWithoutVFaceIsAllowed(t *testing.T) { article, err := buildArticleWithIdentityHeaders( []string{"alt.test"}, "reader@example.org", "Plain post", "body", nil, articleCryptoOptions{Mode: "Plain"}, ) if err != nil { t.Fatal(err) } if strings.Contains(article, "X-VFace-") || strings.Contains(article, "X-Ed25519-Pub:") { t.Fatalf("plain article unexpectedly contains VFace headers:\n%s", article) } } func TestPrepareArticleCryptoSignsCanonicalBody(t *testing.T) { public, private, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } privateSeed := base64.StdEncoding.EncodeToString(private.Seed()) headers, contentType, transferEncoding, wireBody, err := prepareArticleCrypto( "hello\r\nworld", "Sign with Ed25519", articleCryptoOptions{SigningKey: privateSeed}, ) if err != nil { t.Fatal(err) } if contentType != "text/plain; charset=UTF-8" || transferEncoding != "8bit" { t.Fatalf("unexpected MIME metadata: %q, %q", contentType, transferEncoding) } const signaturePrefix = "X-Aegis-Signature: ed25519; " var signature string for _, header := range headers { if strings.HasPrefix(header, signaturePrefix) { signature = strings.TrimPrefix(header, signaturePrefix) } } if signature == "" { t.Fatal("missing Ed25519 signature") } if err := cryptokit.VerifyEd25519([]byte(wireBody), signature, base64.StdEncoding.EncodeToString(public)); err != nil { t.Fatal(err) } } func TestPrepareArticleCryptoRejectsMessageEncryption(t *testing.T) { if _, _, _, _, err := prepareArticleCrypto("secret body", "Encrypt with age", articleCryptoOptions{}); err == nil { t.Fatal("message encryption mode was accepted") } } func TestBuildArticleIncludesToHeader(t *testing.T) { article, err := buildArticleWithIdentityHeaders( []string{"alt.test"}, "reader@example.org", "To header", "body", nil, articleCryptoOptions{Mode: "Plain", To: "mail2news@example.org"}, ) if err != nil { t.Fatal(err) } if !strings.Contains(article, "To: mail2news@example.org\r\n") { t.Fatalf("article missing To header:\n%s", article) } } func TestBuildTextArticleWithVFaceIncludesMandatoryIdentityHeaders(t *testing.T) { if _, err := identity.FindCLI(); err != nil { t.Skip(err) } public, _, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } publicKey := base64.StdEncoding.EncodeToString(public) profile, err := identity.GenerateVFace("pseudonym", "reader@example.org", publicKey) if err != nil { t.Fatal(err) } article, err := buildTextArticleWithVFace( []string{"alt.test"}, "pseudonym ", "VFace test", "body", profile, articleCryptoOptions{Mode: "Plain", ExpectedPublicKey: profile.PublicKey}, ) if err != nil { t.Fatal(err) } for _, want := range []string{ "X-VFace-Version: 1\r\n", "X-Ed25519-Pub: " + publicKey + "\r\n", "Identity-Hash: " + profile.IdentityHash + "\r\n", "X-VFace-Hash: sha256:" + profile.IdentityHash + "\r\n", "X-VFace-PNG-SHA256: " + profile.PNGHash + "\r\n", "X-VFace-Verify: " + identity.VFaceVerificationURL + "\r\n", } { if !strings.Contains(article, want) { t.Fatalf("article missing %q:\n%s", want, article) } } }