Ctrl K
ComunidadGuías…

internal/customfragment/service_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package customfragment

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"encoding/base64"
	"image"
	"image/color"
	"image/png"
	"math/big"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/xssnick/tonutils-go/address"
	"github.com/xssnick/tonutils-go/tvm/cell"

	"telesrv/internal/domain"
)

type fakeLedger struct {
	withdrawal           domain.StarGiftWithdrawal
	completed            bool
	owner                string
	item                 string
	animationKind        domain.StarGiftCollectibleAttributeKind
	animationAttributeID int64
	animationCalls       int
	animationKinds       []domain.StarGiftCollectibleAttributeKind
	animationIDs         []int64
	renderPositions      []float64
}

func (f *fakeLedger) ResolveWithdrawal(_ context.Context, requestID string) (domain.StarGiftWithdrawal, bool, error) {
	if requestID != f.withdrawal.ProviderRequestID {
		return domain.StarGiftWithdrawal{}, false, nil
	}
	return f.withdrawal, true, nil
}

func (f *fakeLedger) CompleteWithdrawalOnChain(_ context.Context, requestID, owner, item string, _ int) (domain.StarGiftWithdrawal, error) {
	if requestID != f.withdrawal.ProviderRequestID {
		return domain.StarGiftWithdrawal{}, ErrInvalidRequest
	}
	f.completed, f.owner, f.item = true, owner, item
	f.withdrawal.Status = "completed"
	f.withdrawal.Gift.OwnerAddress = owner
	f.withdrawal.Gift.GiftAddress = item
	return f.withdrawal, nil
}

func (f *fakeLedger) UniqueBySlug(_ context.Context, slug string) (domain.UniqueStarGift, bool, error) {
	return f.withdrawal.Gift, slug == f.withdrawal.Gift.Slug, nil
}

func (f *fakeLedger) CollectibleAnimationJSON(_ context.Context, _ int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) {
	f.animationKind, f.animationAttributeID = kind, attributeID
	f.animationCalls++
	f.animationKinds = append(f.animationKinds, kind)
	f.animationIDs = append(f.animationIDs, attributeID)
	return []byte(`{"v":"5.5.2"}`), true, nil
}

func (f *fakeLedger) hasAnimationRequest(kind domain.StarGiftCollectibleAttributeKind, attributeID int64) bool {
	for i := range f.animationKinds {
		if f.animationKinds[i] == kind && f.animationIDs[i] == attributeID {
			return true
		}
	}
	return false
}

type fakeVerifier struct {
	item       string
	collection string
	owner      string
	index      *big.Int
	err        error
}

func (f *fakeVerifier) VerifyMint(_ context.Context, collection string, index *big.Int, owner string) (string, error) {
	f.collection, f.index, f.owner = collection, new(big.Int).Set(index), owner
	return f.item, f.err
}
func (*fakeVerifier) Close() {}

func testAddress(fill byte) *address.Address {
	data := make([]byte, 32)
	for i := range data {
		data[i] = fill
	}
	return address.NewAddress(0, 0, data)
}

func testService(t *testing.T, now time.Time) (*Service, *fakeLedger, *fakeVerifier, *address.Address) {
	t.Helper()
	_, privateKey, err := ed25519.GenerateKey(strings.NewReader(strings.Repeat("s", 64)))
	if err != nil {
		t.Fatal(err)
	}
	collection, owner, item := testAddress(1), testAddress(2), testAddress(3)
	ledger := &fakeLedger{withdrawal: domain.StarGiftWithdrawal{
		ProviderRequestID: "withdraw-token", ExpiresAt: int(now.Add(10 * time.Minute).Unix()), Status: "pending",
		Gift: domain.UniqueStarGift{ID: 42, GiftID: 7, Title: "Crystal Star", Slug: "CrystalStar-42", Num: 42,
			Model:    domain.StarGiftCollectibleAttribute{ID: 11, Name: "Azure"},
			Pattern:  domain.StarGiftCollectibleAttribute{ID: 12, Name: "Comet"},
			Backdrop: domain.StarGiftCollectibleAttribute{Name: "Midnight", CenterColor: 0x336699, EdgeColor: 0x112233}},
	}}
	verifier := &fakeVerifier{item: item.StringRaw()}
	service, err := New(Config{
		PublicBaseURL: "https://grams.example", AppName: "Gramsrv", SigningKey: privateKey,
		GiftCollection: collection.String(), MintAmountNanoton: 80_000_000,
		SubwalletID: defaultSubwalletID, AuthorizationTTL: 5 * time.Minute,
		ModelRenderer: func(_ []byte, width, height int, position float64) (*image.NRGBA, error) {
			ledger.renderPositions = append(ledger.renderPositions, position)
			frame := image.NewNRGBA(image.Rect(0, 0, width, height))
			frame.SetNRGBA(width/2, height/2, color.NRGBA{R: 255, A: 255})
			return frame, nil
		},
	}, ledger, verifier, nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(service.Close)
	return service, ledger, verifier, owner
}

func TestIntentBuildsTolkCompatibleSignedPayload(t *testing.T) {
	now := time.Unix(1_800_000_000, 0)
	service, _, _, owner := testService(t, now)
	intent, err := service.Intent(context.Background(), "withdraw-token", owner.String(), now)
	if err != nil {
		t.Fatal(err)
	}
	if intent.Network != mainnetChain || intent.CollectionAddress != service.collection.String() ||
		strings.Contains(intent.CollectionAddress, ":") || intent.Amount != "80000000" ||
		intent.WalletAddress != owner.StringRaw() || intent.ItemIndex != "42" {
		t.Fatalf("unexpected intent: %+v", intent)
	}
	boc, err := base64.StdEncoding.DecodeString(intent.Payload)
	if err != nil {
		t.Fatal(err)
	}
	body, err := cell.FromBOC(boc)
	if err != nil {
		t.Fatal(err)
	}
	slice, err := body.BeginParse()
	if err != nil {
		t.Fatal(err)
	}
	opcode, err := slice.LoadUInt(32)
	if err != nil || opcode != mintOpcode {
		t.Fatalf("opcode=%x err=%v", opcode, err)
	}
	signature, err := slice.LoadSlice(512)
	if err != nil {
		t.Fatal(err)
	}
	authorization, err := slice.LoadRefCell()
	if err != nil {
		t.Fatal(err)
	}
	if !ed25519.Verify(service.publicKey, authorization.Hash(), signature) {
		t.Fatal("payload signature does not cover the authorization cell")
	}
	auth, err := authorization.BeginParse()
	if err != nil {
		t.Fatal(err)
	}
	subwallet, _ := auth.LoadUInt(32)
	_, _ = auth.LoadUInt(32)
	validTill, _ := auth.LoadUInt(32)
	index, _ := auth.LoadBigUInt(256)
	parsedOwner, err := auth.LoadAddr()
	if err != nil {
		t.Fatal(err)
	}
	content, err := auth.LoadRefCell()
	if err != nil {
		t.Fatal(err)
	}
	contentSlice, err := content.BeginParse()
	if err != nil {
		t.Fatal(err)
	}
	prefix, _ := contentSlice.LoadUInt(8)
	metadata, err := contentSlice.LoadStringSnake()
	if err != nil {
		t.Fatal(err)
	}
	if subwallet != uint64(defaultSubwalletID) || validTill != uint64(intent.ValidUntil) ||
		index.Cmp(big.NewInt(42)) != 0 || !parsedOwner.Equals(owner) || prefix != 1 ||
		metadata != "https://grams.example/custom-fragment/metadata/gift/CrystalStar-42.json" {
		t.Fatalf("authorization mismatch: subwallet=%x validTill=%d index=%s owner=%s prefix=%d metadata=%q",
			subwallet, validTill, index, parsedOwner.StringRaw(), prefix, metadata)
	}
}

func TestConfirmCompletesOnlyAfterVerifierMatches(t *testing.T) {
	now := time.Unix(1_800_000_000, 0)
	service, ledger, verifier, owner := testService(t, now)
	confirmation, err := service.Confirm(context.Background(), "withdraw-token", owner.String(), now)
	if err != nil {
		t.Fatal(err)
	}
	if !ledger.completed || verifier.index.Cmp(big.NewInt(42)) != 0 || verifier.owner != owner.StringRaw() ||
		confirmation.Status != "completed" || confirmation.GiftAddress != verifier.item {
		t.Fatalf("confirmation=%+v ledger=%+v verifier=%+v", confirmation, ledger, verifier)
	}
}

func TestHTTPPageAndMetadata(t *testing.T) {
	now := time.Now()
	service, ledger, _, _ := testService(t, now)
	page := httptest.NewRecorder()
	service.ServeWithdrawalPage(page, httptest.NewRequest(http.MethodGet, "/gift-withdrawal/withdraw-token", nil), "withdraw-token")
	if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), "TON Mainnet") || !strings.Contains(page.Body.String(), "Mint gift on TON") {
		t.Fatalf("page status=%d body=%q", page.Code, page.Body.String())
	}
	if strings.Contains(page.Body.String(), string(service.privateKey)) {
		t.Fatal("withdrawal page exposed the private signing key")
	}

	metadata := httptest.NewRecorder()
	service.ServeHTTP(metadata, httptest.NewRequest(http.MethodGet, "/custom-fragment/metadata/gift/CrystalStar-42.json", nil))
	if metadata.Code != http.StatusOK || !strings.Contains(metadata.Body.String(), `"name":"CrystalStar-42"`) ||
		!strings.Contains(metadata.Body.String(), `"image":"https://grams.example/custom-fragment/media/gift/CrystalStar-42.png"`) ||
		!strings.Contains(metadata.Body.String(), `"lottie":"https://grams.example/custom-fragment/media/gift/CrystalStar-42.lottie.json"`) ||
		!strings.Contains(metadata.Body.String(), `"trait_type":"Model"`) {
		t.Fatalf("metadata status=%d body=%q", metadata.Code, metadata.Body.String())
	}

	animation := httptest.NewRecorder()
	service.ServeHTTP(animation, httptest.NewRequest(http.MethodGet, "/custom-fragment/media/gift/CrystalStar-42.lottie.json", nil))
	if animation.Code != http.StatusOK || animation.Header().Get("Content-Type") != "application/json" ||
		animation.Body.String() != `{"v":"5.5.2"}` {
		t.Fatalf("animation status=%d content-type=%q body=%q", animation.Code, animation.Header().Get("Content-Type"), animation.Body.String())
	}
	animationCalls := ledger.animationCalls
	animationCached := httptest.NewRecorder()
	service.ServeHTTP(animationCached, httptest.NewRequest(http.MethodGet, "/custom-fragment/media/gift/CrystalStar-42.lottie.json", nil))
	if animationCached.Code != http.StatusOK || ledger.animationCalls != animationCalls {
		t.Fatalf("cached animation status=%d calls=%d want=%d", animationCached.Code, ledger.animationCalls, animationCalls)
	}

	poster := httptest.NewRecorder()
	service.ServeHTTP(poster, httptest.NewRequest(http.MethodGet, "/custom-fragment/media/gift/CrystalStar-42.png", nil))
	if poster.Code != http.StatusOK || poster.Header().Get("Content-Type") != "image/png" {
		t.Fatalf("poster status=%d content-type=%q body=%q", poster.Code, poster.Header().Get("Content-Type"), poster.Body.String())
	}
	decoded, err := png.Decode(bytes.NewReader(poster.Body.Bytes()))
	if err != nil {
		t.Fatal(err)
	}
	if !ledger.hasAnimationRequest(domain.StarGiftCollectibleModel, 11) || !ledger.hasAnimationRequest(domain.StarGiftCollectiblePattern, 12) {
		t.Fatalf("poster animation requests = %#v/%#v; want model and pattern", ledger.animationKinds, ledger.animationIDs)
	}
	if len(ledger.renderPositions) == 0 || ledger.renderPositions[0] != 0 {
		t.Fatalf("first render position = %#v; want rest frame 0", ledger.renderPositions)
	}
	r, g, b, a := decoded.At(512, 512).RGBA()
	if r != 0xffff || g != 0 || b != 0 || a != 0xffff {
		t.Fatalf("poster model pixel = %04x/%04x/%04x/%04x", r, g, b, a)
	}
}

func TestParseMainnetAddressRejectsTestnetAndMasterchain(t *testing.T) {
	if _, err := parseMainnetAddress(testAddress(4).Testnet(true).String()); err == nil {
		t.Fatal("accepted a testnet-only friendly address")
	}
	masterchain := address.NewAddress(0, byte(0xff), make([]byte, 32))
	if _, err := parseMainnetAddress(masterchain.StringRaw()); err == nil {
		t.Fatal("accepted a masterchain address")
	}
}