diff --git a/demo/demo.sql b/demo/demo.sql index bc20b63..12c4624 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -27,6 +27,8 @@ values (52, 42, 'employee') , (52, 43, 'admin') ; +select setup_redsys(52, '361716962', '1', 'test', 'redirect', 'sq7HjrUOBfKmC576ILgskD5srU870gJ7'); + alter table media alter column media_id restart with 62; select add_media(52, 'plots.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/plots.avif]])', 'base64')); select add_media(52, 'safari_tents.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/safari_tents.avif]])', 'base64')); diff --git a/deploy/available_currencies.sql b/deploy/available_currencies.sql index 03a7e68..146ae04 100644 --- a/deploy/available_currencies.sql +++ b/deploy/available_currencies.sql @@ -4,9 +4,9 @@ begin; -insert into camper.currency(currency_code, currency_symbol) -values ('EUR', '€') - , ('USD', '$') +insert into camper.currency(currency_code, currency_symbol, redsys_code) +values ('EUR', '€', 978) + , ('USD', '$', 840) ; commit; diff --git a/deploy/available_languages.sql b/deploy/available_languages.sql index 50508b6..d5d860a 100644 --- a/deploy/available_languages.sql +++ b/deploy/available_languages.sql @@ -4,11 +4,11 @@ begin; -insert into public.language (lang_tag, name, endonym, selectable, currency_pattern) -values ('und', 'Undefined', 'Undefined', false, '%[3]s%.[1]*[2]f') - , ('ca', 'Catalan', 'català', true, '%.[1]*[2]f %[3]s') - , ('en', 'English', 'English', true, '%[3]s%.[1]*[2]f') - , ('es', 'Spanish', 'español', true, '%.[1]*[2]f %[3]s') +insert into public.language (lang_tag, name, endonym, selectable, currency_pattern, redsys_code) +values ('und', 'Undefined', 'Undefined', false, '%[3]s%.[1]*[2]f', 0) + , ('ca', 'Catalan', 'català', true, '%.[1]*[2]f %[3]s', 3) + , ('en', 'English', 'English', true, '%[3]s%.[1]*[2]f', 2) + , ('es', 'Spanish', 'español', true, '%.[1]*[2]f %[3]s', 1) ; commit; diff --git a/deploy/currency.sql b/deploy/currency.sql index dfb00b8..80d3dc4 100644 --- a/deploy/currency.sql +++ b/deploy/currency.sql @@ -10,9 +10,11 @@ set search_path to camper, public; create table currency ( currency_code currency_code primary key, currency_symbol text not null, - decimal_digits integer not null default 2 + decimal_digits integer not null default 2, + redsys_code integer not null -- Seem to be the same as ISO 4217 ); +grant select on table currency to guest; grant select on table currency to employee; grant select on table currency to admin; diff --git a/deploy/encode_base64url.sql b/deploy/encode_base64url.sql new file mode 100644 index 0000000..e061cac --- /dev/null +++ b/deploy/encode_base64url.sql @@ -0,0 +1,22 @@ +-- Deploy camper:encode_base64url to pg +-- requires: roles +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create or replace function encode_base64url(data bytea) returns text as +$$ + select translate(encode(data, 'base64'), E'+/\n', '-_'); +$$ + language sql + immutable +; + +revoke execute on function encode_base64url(bytea) from public; +grant execute on function encode_base64url(bytea) to guest; +grant execute on function encode_base64url(bytea) to employee; +grant execute on function encode_base64url(bytea) to admin; + +commit; diff --git a/deploy/encrypt_password.sql b/deploy/encrypt_password.sql index 3c4250f..28012e1 100644 --- a/deploy/encrypt_password.sql +++ b/deploy/encrypt_password.sql @@ -11,7 +11,7 @@ create or replace function encrypt_password() returns trigger as $$ begin if tg_op = 'INSERT' or new.password <> old.password then - new.password = crypt(new.password, gen_salt('bf')); + new.password = public.crypt(new.password, public.gen_salt('bf')); end if; return new; end; diff --git a/deploy/extension_pgcrypto.sql b/deploy/extension_pgcrypto.sql index 89aaacf..5331ede 100644 --- a/deploy/extension_pgcrypto.sql +++ b/deploy/extension_pgcrypto.sql @@ -3,6 +3,6 @@ begin; -create extension if not exists pgcrypto with schema auth; +create extension if not exists pgcrypto; commit; diff --git a/deploy/language.sql b/deploy/language.sql index 8bb76c8..f7e90c9 100644 --- a/deploy/language.sql +++ b/deploy/language.sql @@ -12,7 +12,8 @@ create table language name text not null, endonym text not null, selectable boolean not null, - currency_pattern text not null + currency_pattern text not null, + redsys_code integer not null ); grant select on table language to guest; diff --git a/deploy/login.sql b/deploy/login.sql index 99c18b7..2b203be 100644 --- a/deploy/login.sql +++ b/deploy/login.sql @@ -20,7 +20,7 @@ begin if not exists (select * from "user" where "user".email = login.email - and "user".password = crypt(login.password, "user".password)) then + and "user".password = public.crypt(login.password, "user".password)) then insert into login_attempt (user_name, ip_address, success) values (login.email, login.ip_address, false); return ''; @@ -34,7 +34,7 @@ begin and length(cookie) > 30; if user_cookie is null then - select encode(gen_random_bytes(25), 'hex') into user_cookie; + select encode(public.gen_random_bytes(25), 'hex') into user_cookie; end if; update "user" diff --git a/deploy/parse_price.sql b/deploy/parse_price.sql index f9d5fd6..ead0f75 100644 --- a/deploy/parse_price.sql +++ b/deploy/parse_price.sql @@ -51,6 +51,7 @@ comment on function parse_price(text, integer) is 'Converts the string representation of a price in decimal form to cents, according to the number of decimal digits passed.'; revoke execute on function parse_price(text, integer) from public; +grant execute on function parse_price(text, integer) to guest; grant execute on function parse_price(text, integer) to employee; grant execute on function parse_price(text, integer) to admin; diff --git a/deploy/redsys.sql b/deploy/redsys.sql new file mode 100644 index 0000000..75ee913 --- /dev/null +++ b/deploy/redsys.sql @@ -0,0 +1,61 @@ +-- Deploy camper:redsys to pg +-- requires: roles +-- requires: schema_camper +-- requires: company +-- requires: redsys_environment +-- requires: redsys_integration +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table redsys ( + company_id integer primary key references company, + merchant_code text not null constraint merchant_code_valid check (merchant_code ~ '^\d{9}$'), + terminal_number integer not null constraint terminal_number_in_range check(terminal_number > 0 and terminal_number < 1000), + environment redsys_environment not null, + integration redsys_integration not null, + encrypt_key bytea not null +); + +grant select (company_id, merchant_code, terminal_number, environment, integration) on table redsys to guest; +grant select (company_id, merchant_code, terminal_number, environment, integration) on table redsys to employee; +grant select (company_id, merchant_code, terminal_number, environment, integration) on table redsys to admin; +grant update (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) on table redsys to admin; +grant insert (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) on table redsys to admin; +grant delete on table redsys to admin; + +alter table redsys enable row level security; + +create policy guest_ok +on redsys +for select +using (true) +; + +create policy insert_to_company +on redsys +for insert +with check ( + company_id in (select company_id from user_profile) +) +; + +create policy update_company +on redsys +for update +using ( + company_id in (select company_id from user_profile) +) +; + +create policy delete_from_company +on redsys +for delete +using ( + company_id in (select company_id from user_profile) +) +; + +commit; diff --git a/deploy/redsys_encrypt.sql b/deploy/redsys_encrypt.sql new file mode 100644 index 0000000..91e2e3f --- /dev/null +++ b/deploy/redsys_encrypt.sql @@ -0,0 +1,29 @@ +-- Deploy camper:redsys_encrypt to pg +-- requires: roles +-- requires: schema_camper +-- requires: zero_pad +-- requires: redsys +-- requires: extension_pgcrypto + +begin; + +set search_path to camper, public; + +create or replace function redsys_encrypt(company_id integer, data bytea) returns bytea as +$$ + select public.encrypt_iv(zero_pad(data, 8), encrypt_key, '\x0000000000000000'::bytea, '3des-cbc/pad:none') + from redsys + where redsys.company_id = redsys_encrypt.company_id; +$$ + language sql + stable + security definer + set search_path = camper, pg_temp; +; + +revoke execute on function redsys_encrypt(integer, bytea) from public; +grant execute on function redsys_encrypt(integer, bytea) to guest; +grant execute on function redsys_encrypt(integer, bytea) to employee; +grant execute on function redsys_encrypt(integer, bytea) to admin; + +commit; diff --git a/deploy/redsys_environment.sql b/deploy/redsys_environment.sql new file mode 100644 index 0000000..caae000 --- /dev/null +++ b/deploy/redsys_environment.sql @@ -0,0 +1,10 @@ +-- Deploy camper:redsys_environment to pg +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create type redsys_environment as enum ('test', 'live'); + +commit; diff --git a/deploy/redsys_integration.sql b/deploy/redsys_integration.sql new file mode 100644 index 0000000..a771c4e --- /dev/null +++ b/deploy/redsys_integration.sql @@ -0,0 +1,13 @@ +-- Deploy camper:redsys_integration to pg +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create type redsys_integration as enum ( + 'insite', + 'redirect' +); + +commit; diff --git a/deploy/redsys_request.sql b/deploy/redsys_request.sql new file mode 100644 index 0000000..3a8fc9b --- /dev/null +++ b/deploy/redsys_request.sql @@ -0,0 +1,21 @@ +-- Deploy camper:redsys_request to pg +-- requires: schema_camper +-- requires: extension_uri + +begin; + +set search_path to camper, public; + +create type redsys_request as ( + transaction_type integer, + amount text, + order_number text, + product text, + card_holder text, + success_uri uri, + failure_uri uri, + notification_uri uri, + lang_tag text +); + +commit; diff --git a/deploy/redsys_sign_request.sql b/deploy/redsys_sign_request.sql new file mode 100644 index 0000000..8b2dd4e --- /dev/null +++ b/deploy/redsys_sign_request.sql @@ -0,0 +1,68 @@ +-- Deploy camper:redsys_sign_request to pg +-- requires: roles +-- requires: extension_pgcrypto +-- requires: schema_camper +-- requires: encode_base64url +-- requires: redsys_encrypt +-- requires: redsys_request +-- requires: redsys_signed_request +-- requires: company +-- requires: currency +-- requires: language + +begin; + +set search_path to camper, public; + +create or replace function redsys_sign_request(company_id integer, request redsys_request) returns redsys_signed_request as +$$ + select row( + merchant_parameters, + encode(hmac( + convert_to(merchant_parameters, 'UTF-8'), + redsys_encrypt(company_id, convert_to((request).order_number, 'UTF-8')), + 'sha256' + ), 'base64'), + 'HMAC_SHA256_V1' + ) + from ( + select encode_base64url( + convert_to( + -- not using JSONB because for unit test i need stable key order + json_build_object( + 'Ds_Merchant_MerchantCode', merchant_code, + 'Ds_Merchant_Terminal', terminal_number::text, + 'Ds_Merchant_MerchantName', business_name, + 'Ds_Merchant_TransactionType', (request).transaction_type::text, + 'Ds_Merchant_Amount', parse_price((request).amount, decimal_digits)::text, + 'Ds_Merchant_Currency', currency.redsys_code::text, + 'Ds_Merchant_Order', (request).order_number, + 'Ds_Merchant_Productdescription', (request).product, + 'Ds_Merchant_Titular', (request).card_holder, + 'Ds_Merchant_UrlOK', (request).success_uri::text, + 'Ds_Merchant_UrlKO', (request).failure_uri::text, + 'Ds_Merchant_MerchantURL', (request).notification_uri::text, + 'Ds_Merchant_ConsumerLanguage', language.redsys_code::text + )::text, + 'UTF-8' + ) + ) as merchant_parameters + from redsys + join company using (company_id) + join currency using (currency_code) + , language + where redsys.company_id = redsys_sign_request.company_id + and language.lang_tag = (request).lang_tag + ) as build + ; +$$ + language sql + stable +; + +revoke execute on function redsys_sign_request(integer, redsys_request) from public; +grant execute on function redsys_sign_request(integer, redsys_request) to guest; +grant execute on function redsys_sign_request(integer, redsys_request) to employee; +grant execute on function redsys_sign_request(integer, redsys_request) to admin; + +commit; diff --git a/deploy/redsys_signed_request.sql b/deploy/redsys_signed_request.sql new file mode 100644 index 0000000..b7ade69 --- /dev/null +++ b/deploy/redsys_signed_request.sql @@ -0,0 +1,14 @@ +-- Deploy camper:redsys_signed_request to pg +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create type redsys_signed_request as ( + merchant_parameters text, + signature text, + signature_version text +); + +commit; diff --git a/deploy/setup_redsys.sql b/deploy/setup_redsys.sql new file mode 100644 index 0000000..2b422e3 --- /dev/null +++ b/deploy/setup_redsys.sql @@ -0,0 +1,43 @@ +-- Deploy camper:setup_redsys to pg +-- requires: roles +-- requires: schema_camper +-- requires: redsys +-- requires: redsys_environment +-- requires: redsys_integration + +begin; + +set search_path to camper, public; + +create or replace function setup_redsys(company integer, merchant_code text, terminal_number integer, environment redsys_environment, integration redsys_integration, encrypt_key text) returns void as +$$ +begin + if encrypt_key is null then + update redsys + set merchant_code = setup_redsys.merchant_code, + terminal_number = setup_redsys.terminal_number, + environment = setup_redsys.environment, + integration = setup_redsys.integration, + encrypt_key = coalesce(decode(setup_redsys.encrypt_key, 'base64'), redsys.encrypt_key) + where company_id = company + ; + else + insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) + values (company, merchant_code, terminal_number, environment, integration, decode(encrypt_key, 'base64')) + on conflict (company_id) do update + set merchant_code = excluded.merchant_code, + terminal_number = excluded.terminal_number, + environment = excluded.environment, + integration = excluded.integration, + encrypt_key = coalesce(excluded.encrypt_key, redsys.encrypt_key) + ; + end if; +end +$$ + language plpgsql +; + +revoke execute on function setup_redsys(integer, text, integer, redsys_environment, redsys_integration, text) from public; +grant execute on function setup_redsys(integer, text, integer, redsys_environment, redsys_integration, text) to admin; + +commit; diff --git a/deploy/zero_pad.sql b/deploy/zero_pad.sql new file mode 100644 index 0000000..e1986d4 --- /dev/null +++ b/deploy/zero_pad.sql @@ -0,0 +1,22 @@ +-- Deploy camper:zero_pad to pg +-- requires: roles +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create or replace function zero_pad(data bytea, block_len integer) returns bytea as +$$ + select data || decode(lpad('', ((block_len - (octet_length(data) % block_len)) % block_len) * 2, '0'), 'hex'); +$$ + language sql + immutable +; + +revoke execute on function zero_pad(bytea, integer) from public; +grant execute on function zero_pad(bytea, integer) to guest; +grant execute on function zero_pad(bytea, integer) to employee; +grant execute on function zero_pad(bytea, integer) to admin; + +commit; diff --git a/pkg/booking/public.go b/pkg/booking/public.go index d127a24..c11c410 100644 --- a/pkg/booking/public.go +++ b/pkg/booking/public.go @@ -71,23 +71,19 @@ func makeReservation(w http.ResponseWriter, r *http.Request, user *auth.User, co return } - client, err := redsys.New("361716962", "001", "sq7HjrUOBfKmC576ILgskD5srU870gJ7") - if err != nil { - panic(err) - } schema := httplib.Protocol(r) authority := httplib.Host(r) request := &redsys.Request{ TransactionType: redsys.TransactionTypeCharge, - Amount: 1234, - Currency: redsys.CurrencyEUR, - Order: randomOrderNumber(), + Amount: "12.34", + OrderNumber: randomOrderNumber(), Product: "Test Booking", SuccessURL: fmt.Sprintf("%s://%s/%s/booking/success", schema, authority, user.Locale.Language), FailureURL: fmt.Sprintf("%s://%s/%s/booking/failure", schema, authority, user.Locale.Language), - ConsumerLanguage: redsys.LanguageCatalan, + NotificationURL: fmt.Sprintf("%s://%s/%s/booking/notification", schema, authority, user.Locale.Language), + ConsumerLanguage: user.Language, } - signed, err := client.SignRequest(request) + signed, err := redsys.SignRequest(r.Context(), conn, company, request) if err != nil { panic(err) } diff --git a/pkg/database/db.go b/pkg/database/db.go index 5eec52d..925dc71 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -29,7 +29,7 @@ func New(ctx context.Context, connString string) (*DB, error) { if _, err := conn.Exec(ctx, "set search_path to camper, public"); err != nil { return err } - return nil + return registerTypes(ctx, conn) } config.AfterRelease = func(conn *pgx.Conn) bool { diff --git a/pkg/database/types.go b/pkg/database/types.go new file mode 100644 index 0000000..a24998e --- /dev/null +++ b/pkg/database/types.go @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package database + +import ( + "context" + + "github.com/jackc/pgtype" + "github.com/jackc/pgx/v4" +) + +const ( + RedsysRequestTypeName = "redsys_request" + RedsysSignedRequestTypeName = "redsys_signed_request" +) + +func registerTypes(ctx context.Context, conn *pgx.Conn) error { + uriOID, err := registerType(ctx, conn, &pgtype.Text{}, "uri") + if err != nil { + return err + } + + redsysRequestType, err := pgtype.NewCompositeType( + RedsysRequestTypeName, + []pgtype.CompositeTypeField{ + {"transaction_type", pgtype.Int4OID}, + {"amount", pgtype.TextOID}, + {"order_number", pgtype.TextOID}, + {"product", pgtype.TextOID}, + {"card_holder", pgtype.TextOID}, + {"success_uri", uriOID}, + {"failure_uri", uriOID}, + {"notification_uri", uriOID}, + {"lang_tag", pgtype.TextOID}, + }, + conn.ConnInfo(), + ) + if err != nil { + return err + } + if _, err = registerType(ctx, conn, redsysRequestType, redsysRequestType.TypeName()); err != nil { + return err + } + + redsysSignedRequestType, err := pgtype.NewCompositeType( + RedsysSignedRequestTypeName, + []pgtype.CompositeTypeField{ + {"merchant_parameters", pgtype.TextOID}, + {"signature", pgtype.TextOID}, + {"signature_version", pgtype.TextOID}, + }, + conn.ConnInfo(), + ) + if err != nil { + return err + } + if _, err = registerType(ctx, conn, redsysSignedRequestType, redsysSignedRequestType.TypeName()); err != nil { + return err + } + + return nil +} + +func registerType(ctx context.Context, conn *pgx.Conn, value pgtype.Value, name string) (oid uint32, err error) { + if err = conn.QueryRow(ctx, "select $1::regtype::oid", name).Scan(&oid); err != nil { + return + } + conn.ConnInfo().RegisterDataType(pgtype.DataType{Value: value, Name: name, OID: oid}) + return +} diff --git a/pkg/redsys/client.go b/pkg/redsys/client.go index d5d69a5..ac86fd4 100644 --- a/pkg/redsys/client.go +++ b/pkg/redsys/client.go @@ -6,25 +6,14 @@ package redsys import ( - "bytes" - "crypto/cipher" - "crypto/des" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/json" + "context" "fmt" - "strings" -) -type Language int + "github.com/jackc/pgtype" + "golang.org/x/text/language" -const ( - LanguageDefault Language = 0 - LanguageSpanish Language = 1 - LanguageEnglish Language = 2 - LanguageCatalan Language = 3 - LanguageFrench Language = 4 + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" ) type TransactionType int @@ -39,132 +28,16 @@ const ( TransactionTypeSplitConfirm TransactionType = 8 ) -type Currency int - -// Source: https://sis-d.redsys.es/pagosonline/codigos-autorizacion.html#monedas -// The codes seem to be the same as ISO 4217 https://en.wikipedia.org/wiki/ISO_4217 -const ( - CurrencyEUR Currency = 978 -) - type Request struct { TransactionType TransactionType - Amount int64 - Currency Currency - Order string + Amount string + OrderNumber string Product string CardHolder string SuccessURL string FailureURL string - ConsumerLanguage Language -} - -type merchantParameters struct { - MerchantCode string `json:"Ds_Merchant_MerchantCode"` - Terminal string `json:"Ds_Merchant_Terminal"` - TransactionType TransactionType `json:"Ds_Merchant_TransactionType,string"` - Amount int64 `json:"Ds_Merchant_Amount,string"` - Currency Currency `json:"Ds_Merchant_Currency,string"` - Order string `json:"Ds_Merchant_Order"` - MerchantURL string `json:"Ds_Merchant_MerchantURL,omitempty"` - Product string `json:"Ds_Merchant_ProductDescription,omitempty"` - CardHolder string `json:"Ds_Merchant_Titular,omitempty"` - SuccessURL string `json:"Ds_Merchant_UrlOK,omitempty"` - FailureURL string `json:"Ds_Merchant_UrlKO,omitempty"` - MerchantName string `json:"Ds_Merchant_MerchantName,omitempty"` - ConsumerLanguage Language `json:"Ds_Merchant_ConsumerLanguage,string,omitempty"` -} - -type Client struct { - merchantCode string - terminalCode string - crypto cipher.Block -} - -func New(merchantCode string, terminalCode string, key string) (*Client, error) { - b, err := base64.StdEncoding.DecodeString(key) - if err != nil { - return nil, fmt.Errorf("key is invalid base64: %v", err) - } - - crypto, err := des.NewTripleDESCipher(b) - if err != nil { - return nil, err - } - - return &Client{ - merchantCode: merchantCode, - terminalCode: terminalCode, - crypto: crypto, - }, nil -} - -func (p *Client) SignRequest(req *Request) (*SignedRequest, error) { - signed := &SignedRequest{ - SignatureVersion: "HMAC_SHA256_V1", - } - params := &merchantParameters{ - MerchantCode: p.merchantCode, - Terminal: p.terminalCode, - TransactionType: req.TransactionType, - Amount: req.Amount, - Currency: req.Currency, - Order: req.Order, - MerchantURL: "http://localhost/ca/", - Product: req.Product, - CardHolder: req.CardHolder, - SuccessURL: req.SuccessURL, - FailureURL: req.FailureURL, - MerchantName: "Merchant Here", - ConsumerLanguage: req.ConsumerLanguage, - } - var err error - signed.MerchantParameters, err = encodeMerchantParameters(params) - if err != nil { - return nil, err - } - signed.Signature = p.sign(params.Order, signed.MerchantParameters) - return signed, nil -} - -func encodeMerchantParameters(params *merchantParameters) (string, error) { - marshalled, err := json.Marshal(params) - if err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(marshalled), err -} - -func (p *Client) sign(id string, data string) string { - key := encrypt3DES(id, p.crypto) - return mac256(data, key) -} - -var iv = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - -func encrypt3DES(str string, crypto cipher.Block) string { - cbc := cipher.NewCBCEncrypter(crypto, iv) - - decrypted := []byte(str) - decryptedPadded, _ := zeroPad(decrypted, crypto.BlockSize()) - cbc.CryptBlocks(decryptedPadded, decryptedPadded) - - return base64.StdEncoding.EncodeToString(decryptedPadded) -} - -func zeroPad(data []byte, blockLen int) ([]byte, error) { - padLen := (blockLen - (len(data) % blockLen)) % blockLen - pad := bytes.Repeat([]byte{0x00}, padLen) - - return append(data, pad...), nil -} - -func mac256(data string, key string) string { - decodedKey, _ := base64.StdEncoding.DecodeString(key) - res := hmac.New(sha256.New, decodedKey) - res.Write([]byte(strings.TrimSpace(data))) - result := res.Sum(nil) - return base64.StdEncoding.EncodeToString(result) + NotificationURL string + ConsumerLanguage language.Tag } type SignedRequest struct { @@ -172,3 +45,49 @@ type SignedRequest struct { Signature string SignatureVersion string } + +func SignRequest(ctx context.Context, conn *database.Conn, company *auth.Company, req *Request) (*SignedRequest, error) { + row := conn.QueryRow(ctx, "select redsys_sign_request($1, $2)", company.ID, req) + signed := &SignedRequest{} + if err := row.Scan(&signed); err != nil { + return nil, err + } + return signed, nil +} + +func (src Request) EncodeText(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) { + typeName := database.RedsysRequestTypeName + dt, ok := ci.DataTypeForName(typeName) + if !ok { + return nil, fmt.Errorf("unable to find oid for type name %v", typeName) + } + values := []interface{}{ + src.TransactionType, + src.Amount, + src.OrderNumber, + src.Product, + src.CardHolder, + src.SuccessURL, + src.FailureURL, + src.NotificationURL, + src.ConsumerLanguage, + } + ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType) + if err := ct.Set(values); err != nil { + return nil, err + } + return ct.EncodeText(ci, dst) +} + +func (dst *SignedRequest) DecodeText(ci *pgtype.ConnInfo, src []byte) error { + typeName := database.RedsysSignedRequestTypeName + dt, ok := ci.DataTypeForName(typeName) + if !ok { + return fmt.Errorf("unable to find oid for type name %v", typeName) + } + ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType) + if err := ct.DecodeText(ci, src); err != nil { + return err + } + return ct.AssignTo(dst) +} diff --git a/po/ca.po b/po/ca.po index a2a0b7f..fa7ba25 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-10-19 21:23+0200\n" +"POT-Creation-Date: 2023-10-27 01:12+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -1211,12 +1211,12 @@ msgid "Slide image must be an image media type." msgstr "La imatge de la diapositiva ha de ser un mèdia de tipus imatge." #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:210 -#: pkg/booking/public.go:292 +#: pkg/booking/public.go:288 msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:211 -#: pkg/booking/public.go:293 +#: pkg/booking/public.go:289 msgid "This email is not valid. It should be like name@domain.com." msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com." @@ -1349,7 +1349,7 @@ msgctxt "season" msgid "Closed" msgstr "Tancat" -#: pkg/campsite/admin.go:226 pkg/booking/public.go:321 +#: pkg/campsite/admin.go:226 pkg/booking/public.go:317 msgid "Selected campsite type is not valid." msgstr "El tipus d’allotjament escollit no és vàlid." @@ -1446,7 +1446,7 @@ msgstr "No podeu deixar la data de fi en blanc." msgid "End date must be a valid date." msgstr "La data de fi ha de ser una data vàlida." -#: pkg/company/admin.go:193 pkg/booking/public.go:279 +#: pkg/company/admin.go:193 pkg/booking/public.go:275 msgid "Selected country is not valid." msgstr "El país escollit no és vàlid." @@ -1466,11 +1466,11 @@ msgstr "No podeu deixar el NIF en blanc." msgid "This VAT number is not valid." msgstr "Aquest NIF no és vàlid." -#: pkg/company/admin.go:205 pkg/booking/public.go:295 +#: pkg/company/admin.go:205 pkg/booking/public.go:291 msgid "Phone can not be empty." msgstr "No podeu deixar el telèfon en blanc." -#: pkg/company/admin.go:206 pkg/booking/public.go:296 +#: pkg/company/admin.go:206 pkg/booking/public.go:292 msgid "This phone number is not valid." msgstr "Aquest número de telèfon no és vàlid." @@ -1494,7 +1494,7 @@ msgstr "No podeu deixar la província en blanc." msgid "Postal code can not be empty." msgstr "No podeu deixar el codi postal en blanc." -#: pkg/company/admin.go:220 pkg/booking/public.go:288 +#: pkg/company/admin.go:220 pkg/booking/public.go:284 msgid "This postal code is not valid." msgstr "Aquest codi postal no és vàlid." @@ -1522,102 +1522,102 @@ msgstr "No podeu deixar el fitxer del mèdia en blanc." msgid "Filename can not be empty." msgstr "No podeu deixar el nom del fitxer en blanc." -#: pkg/booking/public.go:283 +#: pkg/booking/public.go:279 msgid "Full name can not be empty." msgstr "No podeu deixar el nom i els cognoms en blanc." -#: pkg/booking/public.go:284 +#: pkg/booking/public.go:280 msgid "Full name must have at least one letter." msgstr "El nom i els cognoms han de tenir com a mínim una lletra." -#: pkg/booking/public.go:300 +#: pkg/booking/public.go:296 msgid "Number of adults can not be empty" msgstr "No podeu deixar el número d’adults en blanc." -#: pkg/booking/public.go:301 +#: pkg/booking/public.go:297 msgid "Number of adults must be an integer." msgstr "El número d’adults ha de ser enter." -#: pkg/booking/public.go:302 +#: pkg/booking/public.go:298 msgid "Number of adults must be one or greater." msgstr "El número d’adults no pot ser zero." -#: pkg/booking/public.go:305 +#: pkg/booking/public.go:301 msgid "Number of teenagers can not be empty" msgstr "No podeu deixar el número d’adolescents en blanc." -#: pkg/booking/public.go:306 +#: pkg/booking/public.go:302 msgid "Number of teenagers must be an integer." msgstr "El número d’adolescents ha de ser enter." -#: pkg/booking/public.go:307 +#: pkg/booking/public.go:303 msgid "Number of teenagers must be zero or greater." msgstr "El número d’adolescents ha de ser com a mínim zero." -#: pkg/booking/public.go:310 +#: pkg/booking/public.go:306 msgid "Number of children can not be empty" msgstr "No podeu deixar el número de nens en blanc." -#: pkg/booking/public.go:311 +#: pkg/booking/public.go:307 msgid "Number of children must be an integer." msgstr "El número de nens ha de ser enter." -#: pkg/booking/public.go:312 +#: pkg/booking/public.go:308 msgid "Number of children must be zero or greater." msgstr "El número de nens ha de ser com a mínim zero." -#: pkg/booking/public.go:315 +#: pkg/booking/public.go:311 msgid "Number of dogs can not be empty" msgstr "No podeu deixar el número de gossos en blanc." -#: pkg/booking/public.go:316 +#: pkg/booking/public.go:312 msgid "Number of dogs must be an integer." msgstr "El número de gossos ha de ser enter." -#: pkg/booking/public.go:317 +#: pkg/booking/public.go:313 msgid "Number of dogs must be zero or greater." msgstr "El número de gossos ha de ser com a mínim zero." -#: pkg/booking/public.go:322 +#: pkg/booking/public.go:318 msgid "Arrival date can not be empty" msgstr "No podeu deixar la data d’arribada en blanc." -#: pkg/booking/public.go:323 +#: pkg/booking/public.go:319 msgid "Arrival date must be a valid date." msgstr "La data d’arribada ha de ser una data vàlida." -#: pkg/booking/public.go:327 +#: pkg/booking/public.go:323 msgid "Departure date can not be empty" msgstr "No podeu deixar la data de sortida en blanc." -#: pkg/booking/public.go:328 +#: pkg/booking/public.go:324 msgid "Departure date must be a valid date." msgstr "La data de sortida ha de ser una data vàlida." -#: pkg/booking/public.go:329 +#: pkg/booking/public.go:325 msgid "The departure date must be after the arrival date." msgstr "La data de sortida ha de ser posterior a la d’arribada." -#: pkg/booking/public.go:332 +#: pkg/booking/public.go:328 msgid "It is mandatory to agree to the reservation conditions." msgstr "És obligatori acceptar les condicions de reserves." -#: pkg/booking/public.go:335 +#: pkg/booking/public.go:331 #, c-format msgid "%s can not be empty" msgstr "No podeu deixar %s en blanc." -#: pkg/booking/public.go:336 +#: pkg/booking/public.go:332 #, c-format msgid "%s must be an integer." msgstr "%s ha de ser un número enter." -#: pkg/booking/public.go:337 +#: pkg/booking/public.go:333 #, c-format msgid "%s must be %d or greater." msgstr "El valor de %s ha de ser com a mínim %d." -#: pkg/booking/public.go:338 +#: pkg/booking/public.go:334 #, c-format msgid "%s must be at most %d." msgstr "El valor de %s ha de ser com a màxim %d." diff --git a/po/es.po b/po/es.po index 089ceb6..6fdbae1 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-10-19 21:23+0200\n" +"POT-Creation-Date: 2023-10-27 01:12+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -1211,12 +1211,12 @@ msgid "Slide image must be an image media type." msgstr "La imagen de la diapositiva tiene que ser un medio de tipo imagen." #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:210 -#: pkg/booking/public.go:292 +#: pkg/booking/public.go:288 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:211 -#: pkg/booking/public.go:293 +#: pkg/booking/public.go:289 msgid "This email is not valid. It should be like name@domain.com." msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com." @@ -1349,7 +1349,7 @@ msgctxt "season" msgid "Closed" msgstr "Cerrado" -#: pkg/campsite/admin.go:226 pkg/booking/public.go:321 +#: pkg/campsite/admin.go:226 pkg/booking/public.go:317 msgid "Selected campsite type is not valid." msgstr "El tipo de alojamiento escogido no es válido." @@ -1446,7 +1446,7 @@ msgstr "No podéis dejar la fecha final en blanco." msgid "End date must be a valid date." msgstr "La fecha final tiene que ser una fecha válida." -#: pkg/company/admin.go:193 pkg/booking/public.go:279 +#: pkg/company/admin.go:193 pkg/booking/public.go:275 msgid "Selected country is not valid." msgstr "El país escogido no es válido." @@ -1466,11 +1466,11 @@ msgstr "No podéis dejar el NIF en blanco." msgid "This VAT number is not valid." msgstr "Este NIF no es válido." -#: pkg/company/admin.go:205 pkg/booking/public.go:295 +#: pkg/company/admin.go:205 pkg/booking/public.go:291 msgid "Phone can not be empty." msgstr "No podéis dejar el teléfono en blanco." -#: pkg/company/admin.go:206 pkg/booking/public.go:296 +#: pkg/company/admin.go:206 pkg/booking/public.go:292 msgid "This phone number is not valid." msgstr "Este teléfono no es válido." @@ -1494,7 +1494,7 @@ msgstr "No podéis dejar la provincia en blanco." msgid "Postal code can not be empty." msgstr "No podéis dejar el código postal en blanco." -#: pkg/company/admin.go:220 pkg/booking/public.go:288 +#: pkg/company/admin.go:220 pkg/booking/public.go:284 msgid "This postal code is not valid." msgstr "Este código postal no es válido." @@ -1522,101 +1522,102 @@ msgstr "No podéis dejar el archivo del medio en blanco." msgid "Filename can not be empty." msgstr "No podéis dejar el nombre del archivo en blanco." -#: pkg/booking/public.go:283 +#: pkg/booking/public.go:279 msgid "Full name can not be empty." msgstr "No podéis dejar el nombre y los apellidos en blanco." -#: pkg/booking/public.go:284 +#: pkg/booking/public.go:280 msgid "Full name must have at least one letter." msgstr "El nombre y los apellidos tienen que tener como mínimo una letra." -#: pkg/booking/public.go:300 +#: pkg/booking/public.go:296 msgid "Number of adults can not be empty" msgstr "No podéis dejar el número de adultos blanco." -#: pkg/booking/public.go:301 +#: pkg/booking/public.go:297 msgid "Number of adults must be an integer." msgstr "El número de adultos tiene que ser entero." -#: pkg/booking/public.go:302 +#: pkg/booking/public.go:298 msgid "Number of adults must be one or greater." msgstr "El número de adultos no puede ser cero." -#: pkg/booking/public.go:305 +#: pkg/booking/public.go:301 msgid "Number of teenagers can not be empty" msgstr "No podéis dejar el número de adolescentes en blanco." -#: pkg/booking/public.go:306 +#: pkg/booking/public.go:302 msgid "Number of teenagers must be an integer." msgstr "El número de adolescentes tiene que ser entero." -#: pkg/booking/public.go:307 +#: pkg/booking/public.go:303 msgid "Number of teenagers must be zero or greater." msgstr "El número de adolescentes tiene que ser como mínimo cero." -#: pkg/booking/public.go:310 +#: pkg/booking/public.go:306 msgid "Number of children can not be empty" msgstr "No podéis dejar el número de niños en blanco." -#: pkg/booking/public.go:311 +#: pkg/booking/public.go:307 msgid "Number of children must be an integer." msgstr "El número de niños tiene que ser entero." -#: pkg/booking/public.go:312 +#: pkg/booking/public.go:308 msgid "Number of children must be zero or greater." msgstr "El número de niños tiene que ser como mínimo cero." -#: pkg/booking/public.go:315 +#: pkg/booking/public.go:311 msgid "Number of dogs can not be empty" msgstr "No podéis dejar el número de perros en blanco." -#: pkg/booking/public.go:316 +#: pkg/booking/public.go:312 msgid "Number of dogs must be an integer." msgstr "El número de perros tiene que ser entero." -#: pkg/booking/public.go:317 +#: pkg/booking/public.go:313 msgid "Number of dogs must be zero or greater." msgstr "El número de perros tiene que ser como mínimo cero." -#: pkg/booking/public.go:322 +#: pkg/booking/public.go:318 msgid "Arrival date can not be empty" msgstr "No podéis dejar la fecha de llegada en blanco." -#: pkg/booking/public.go:323 +#: pkg/booking/public.go:319 msgid "Arrival date must be a valid date." msgstr "La fecha de llegada tiene que ser una fecha válida." -#: pkg/booking/public.go:327 +#: pkg/booking/public.go:323 msgid "Departure date can not be empty" msgstr "No podéis dejar la fecha de partida en blanco." -#: pkg/booking/public.go:328 +#: pkg/booking/public.go:324 msgid "Departure date must be a valid date." msgstr "La fecha de partida tiene que ser una fecha válida." -#: pkg/booking/public.go:329 +#: pkg/booking/public.go:325 msgid "The departure date must be after the arrival date." msgstr "La fecha de partida tiene que ser posterior a la de llegada." -#: pkg/booking/public.go:332 +#: pkg/booking/public.go:328 msgid "It is mandatory to agree to the reservation conditions." msgstr "Es obligatorio aceptar las condiciones de reserva." -#: pkg/booking/public.go:335 +#: pkg/booking/public.go:331 +#, c-format msgid "%s can not be empty" msgstr "No podéis dejar %s en blanco." -#: pkg/booking/public.go:336 +#: pkg/booking/public.go:332 #, c-format msgid "%s must be an integer." msgstr "%s tiene que ser un número entero." -#: pkg/booking/public.go:337 +#: pkg/booking/public.go:333 #, c-format msgid "%s must be %d or greater." msgstr "%s tiene que ser como mínimo %d." -#: pkg/booking/public.go:338 +#: pkg/booking/public.go:334 #, c-format msgid "%s must be at most %d." msgstr "%s tiene que ser como máximo %d" diff --git a/revert/encode_base64url.sql b/revert/encode_base64url.sql new file mode 100644 index 0000000..3fb5505 --- /dev/null +++ b/revert/encode_base64url.sql @@ -0,0 +1,7 @@ +-- Revert camper:encode_base64url from pg + +begin; + +drop function if exists camper.encode_base64url(bytea); + +commit; diff --git a/revert/redsys.sql b/revert/redsys.sql new file mode 100644 index 0000000..42e7904 --- /dev/null +++ b/revert/redsys.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys from pg + +begin; + +drop table if exists camper.redsys; + +commit; diff --git a/revert/redsys_encrypt.sql b/revert/redsys_encrypt.sql new file mode 100644 index 0000000..a43469f --- /dev/null +++ b/revert/redsys_encrypt.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_encrypt from pg + +begin; + +drop function if exists camper.redsys_encrypt(integer, bytea); + +commit; diff --git a/revert/redsys_environment.sql b/revert/redsys_environment.sql new file mode 100644 index 0000000..57365d4 --- /dev/null +++ b/revert/redsys_environment.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_environment from pg + +begin; + +drop type if exists camper.redsys_environment; + +commit; diff --git a/revert/redsys_integration.sql b/revert/redsys_integration.sql new file mode 100644 index 0000000..9330a7e --- /dev/null +++ b/revert/redsys_integration.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_integration from pg + +begin; + +drop type if exists camper.redsys_integration; + +commit; diff --git a/revert/redsys_request.sql b/revert/redsys_request.sql new file mode 100644 index 0000000..c2c0462 --- /dev/null +++ b/revert/redsys_request.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_request from pg + +begin; + +drop type if exists camper.redsys_request; + +commit; diff --git a/revert/redsys_sign_request.sql b/revert/redsys_sign_request.sql new file mode 100644 index 0000000..0021d65 --- /dev/null +++ b/revert/redsys_sign_request.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_sign_request from pg + +begin; + +drop function if exists camper.redsys_sign_request(integer, camper.redsys_request); + +commit; diff --git a/revert/redsys_signed_request.sql b/revert/redsys_signed_request.sql new file mode 100644 index 0000000..c152a04 --- /dev/null +++ b/revert/redsys_signed_request.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_signed_request from pg + +begin; + +drop type if exists camper.redsys_signed_request; + +commit; diff --git a/revert/setup_redsys.sql b/revert/setup_redsys.sql new file mode 100644 index 0000000..cfb7cee --- /dev/null +++ b/revert/setup_redsys.sql @@ -0,0 +1,7 @@ +-- Revert camper:setup_redsys from pg + +begin; + +drop function if exists camper.setup_redsys(integer, text, integer, camper.redsys_environment, camper.redsys_integration, text); + +commit; diff --git a/revert/zero_pad.sql b/revert/zero_pad.sql new file mode 100644 index 0000000..82f45f1 --- /dev/null +++ b/revert/zero_pad.sql @@ -0,0 +1,7 @@ +-- Revert camper:zero_pad from pg + +begin; + +drop function if exists camper.zero_pad(bytea, integer); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 808bc9c..c7c1b7c 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -109,3 +109,13 @@ campsite_type_feature_i18n [roles schema_camper campsite_type_feature language] add_campsite_type_feature [roles schema_camper campsite_type_feature campsite_type] 2023-10-13T16:26:04Z jordi fita mas # Add function to create new campsite type features edit_campsite_type_feature [roles schema_camper campsite_type_feature] 2023-10-13T16:37:43Z jordi fita mas # Add function to update campsite type features translate_campsite_type_feature [roles schema_camper campsite_type_feature_i18n] 2023-10-13T16:43:55Z jordi fita mas # Add function to translate campsite type features +redsys_environment [schema_camper] 2023-10-26T17:55:13Z jordi fita mas # Add enumeration of Redsys environments +redsys_integration [schema_camper] 2023-10-26T18:10:21Z jordi fita mas # Add enumeration for Redsys integrations +redsys [roles schema_camper company redsys_environment redsys_integration user_profile] 2023-10-26T16:26:23Z jordi fita mas # Add the relation for Redsys settings +setup_redsys [roles schema_camper redsys redsys_environment redsys_integration] 2023-10-26T17:20:29Z jordi fita mas # Add function to setup Redsys parameters +redsys_request [schema_camper extension_uri] 2023-10-26T18:50:55Z jordi fita mas # Add the composite type of a Redsys request +redsys_signed_request [schema_camper] 2023-10-26T18:54:48Z jordi fita mas # Add the composite type of a Redsys signed request +zero_pad [roles schema_camper] 2023-10-26T19:57:00Z jordi fita mas # Add function to pad a bytea with zeros in front +redsys_encrypt [roles schema_camper zero_pad redsys extension_pgcrypto] 2023-10-26T20:18:00Z jordi fita mas # Add function to encrypt data with Redsys’ encrypt key +encode_base64url [roles schema_camper] 2023-10-26T21:00:47Z jordi fita mas # Add function to encode bytea the so-called base64url +redsys_sign_request [roles extension_pgcrypto schema_camper encode_base64url redsys_encrypt redsys_request redsys_signed_request company currency language] 2023-10-26T21:12:01Z jordi fita mas # Add the function that signs Redsys requests diff --git a/test/currency.sql b/test/currency.sql index 73b735a..e8162e1 100644 --- a/test/currency.sql +++ b/test/currency.sql @@ -5,13 +5,13 @@ reset client_min_messages; begin; -select plan(20); +select plan(24); set search_path to camper, public; select has_table('currency'); select has_pk('currency' ); -select table_privs_are('currency', 'guest', array []::text[]); +select table_privs_are('currency', 'guest', array ['SELECT']); select table_privs_are('currency', 'employee', array ['SELECT']); select table_privs_are('currency', 'admin', array ['SELECT']); select table_privs_are('currency', 'authenticator', array []::text[]); @@ -33,6 +33,11 @@ select col_not_null('currency', 'decimal_digits'); select col_has_default('currency', 'decimal_digits'); select col_default_is('currency', 'decimal_digits', 2); +select has_column('currency', 'redsys_code'); +select col_type_is('currency', 'redsys_code', 'integer'); +select col_not_null('currency', 'redsys_code'); +select col_hasnt_default('currency', 'redsys_code'); + select * from finish(); diff --git a/test/encode_base64url.sql b/test/encode_base64url.sql new file mode 100644 index 0000000..179b5da --- /dev/null +++ b/test/encode_base64url.sql @@ -0,0 +1,48 @@ +-- Test encode_base64url +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to camper, public; + +select has_function('camper', 'encode_base64url', array['bytea']); +select function_lang_is('camper', 'encode_base64url', array['bytea'], 'sql'); +select function_returns('camper', 'encode_base64url', array['bytea'], 'text'); +select isnt_definer('camper', 'encode_base64url', array['bytea']); +select volatility_is('camper', 'encode_base64url', array['bytea'], 'immutable'); +select function_privs_are('camper', 'encode_base64url', array ['bytea'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'encode_base64url', array ['bytea'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'encode_base64url', array ['bytea'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'encode_base64url', array ['bytea'], 'authenticator', array[]::text[]); + +select is( + encode_base64url(decode('sq7HjrUOBfKmC576ILgskD5srU870gJ7', 'base64')), + 'sq7HjrUOBfKmC576ILgskD5srU870gJ7' +); +select is( + encode_base64url(decode('K+gWWTl+gKlkxEGDsuc0UunIQwoFLM0t', 'base64')), + 'K-gWWTl-gKlkxEGDsuc0UunIQwoFLM0t' +); +select is( + encode_base64url(decode('cQGdqnb4V/nSKe8zTzyP/VaDGd2rAmzt', 'base64')), + 'cQGdqnb4V_nSKe8zTzyP_VaDGd2rAmzt' +); +select is( + encode_base64url(decode('x59e+kbXSpgsmy31V5l2BtTuKv8/pVKg', 'base64')), + 'x59e-kbXSpgsmy31V5l2BtTuKv8_pVKg' +); + +select is( + encode_base64url(decode(E'yvmzOBJpRfBn2XodxH/1WOpbASfcH5KAMRbDNevBV5srR8Yj5wAiP6+Obv0NCpyrOUVeYZ8NqMe/\ntM28EoOY2g==', 'base64')), + 'yvmzOBJpRfBn2XodxH_1WOpbASfcH5KAMRbDNevBV5srR8Yj5wAiP6-Obv0NCpyrOUVeYZ8NqMe_tM28EoOY2g==', + 'Should not include newlines in the output' +); + +select * +from finish(); + +rollback; diff --git a/test/language.sql b/test/language.sql index a53ab09..af57ae0 100644 --- a/test/language.sql +++ b/test/language.sql @@ -7,14 +7,14 @@ begin; set search_path to public; -select plan(27); +select plan(31); select has_table('language'); select has_pk('language'); select table_privs_are('language', 'guest', array ['SELECT']); select table_privs_are('language', 'employee', array ['SELECT']); select table_privs_are('language', 'admin', array ['SELECT']); -select table_privs_are('language', 'authenticator', array ['SELECT']::text[]); +select table_privs_are('language', 'authenticator', array ['SELECT']); select has_column('language', 'lang_tag'); select col_is_pk('language', 'lang_tag'); @@ -42,6 +42,11 @@ select col_type_is('language', 'currency_pattern', 'text'); select col_not_null('language', 'currency_pattern'); select col_hasnt_default('language', 'currency_pattern'); +select has_column('language', 'redsys_code'); +select col_type_is('language', 'redsys_code', 'integer'); +select col_not_null('language', 'redsys_code'); +select col_hasnt_default('language', 'redsys_code'); + select * from finish(); diff --git a/test/parse_price.sql b/test/parse_price.sql index ed5090c..4e21e45 100644 --- a/test/parse_price.sql +++ b/test/parse_price.sql @@ -14,7 +14,7 @@ select function_lang_is('camper', 'parse_price', array ['text', 'integer'], 'plp select function_returns('camper', 'parse_price', array ['text', 'integer'], 'integer'); select isnt_definer('camper', 'parse_price', array ['text', 'integer']); select volatility_is('camper', 'parse_price', array ['text', 'integer'], 'immutable'); -select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'guest', array []::text[]); +select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'guest', array ['EXECUTE']); select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'employee', array ['EXECUTE']); select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'admin', array ['EXECUTE']); select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'authenticator', array []::text[]); diff --git a/test/redsys.sql b/test/redsys.sql new file mode 100644 index 0000000..b983205 --- /dev/null +++ b/test/redsys.sql @@ -0,0 +1,245 @@ +-- Test redsys +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(75); + +set search_path to camper, public; + +select has_table('redsys'); +select has_pk('redsys'); +select table_privs_are('redsys', 'guest', array []::text[]); +select table_privs_are('redsys', 'employee', array []::text[]); +select table_privs_are('redsys', 'admin', array ['DELETE']); +select table_privs_are('redsys', 'authenticator', array []::text[]); + +select has_column('redsys', 'company_id'); +select col_is_pk('redsys', 'company_id'); +select col_is_fk('redsys', 'company_id'); +select fk_ok('redsys', 'company_id', 'company', 'company_id'); +select col_type_is('redsys', 'company_id', 'integer'); +select col_not_null('redsys', 'company_id'); +select col_hasnt_default('redsys', 'company_id'); +select column_privs_are('redsys', 'company_id', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'company_id', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'company_id', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'company_id', 'authenticator', array []::text[]); + +select has_column('redsys', 'merchant_code'); +select col_type_is('redsys', 'merchant_code', 'text'); +select col_not_null('redsys', 'merchant_code'); +select col_hasnt_default('redsys', 'merchant_code'); +select column_privs_are('redsys', 'merchant_code', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'merchant_code', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'merchant_code', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'merchant_code', 'authenticator', array []::text[]); + +select has_column('redsys', 'terminal_number'); +select col_type_is('redsys', 'terminal_number', 'integer'); +select col_not_null('redsys', 'terminal_number'); +select col_hasnt_default('redsys', 'terminal_number'); +select column_privs_are('redsys', 'terminal_number', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'terminal_number', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'terminal_number', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'terminal_number', 'authenticator', array []::text[]); + +select has_column('redsys', 'environment'); +select col_type_is('redsys', 'environment', 'redsys_environment'); +select col_not_null('redsys', 'environment'); +select col_hasnt_default('redsys', 'environment'); +select column_privs_are('redsys', 'environment', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'environment', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'environment', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'environment', 'authenticator', array []::text[]); + +select has_column('redsys', 'integration'); +select col_type_is('redsys', 'integration', 'redsys_integration'); +select col_not_null('redsys', 'integration'); +select col_hasnt_default('redsys', 'integration'); +select column_privs_are('redsys', 'integration', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'integration', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'integration', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'integration', 'authenticator', array []::text[]); + +select has_column('redsys', 'encrypt_key'); +select col_type_is('redsys', 'encrypt_key', 'bytea'); +select col_not_null('redsys', 'encrypt_key'); +select col_hasnt_default('redsys', 'encrypt_key'); +select column_privs_are('redsys', 'encrypt_key', 'guest', array []::text[]); +select column_privs_are('redsys', 'encrypt_key', 'employee', array []::text[]); +select column_privs_are('redsys', 'encrypt_key', 'admin', array ['INSERT', 'UPDATE']); +select column_privs_are('redsys', 'encrypt_key', 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate redsys cascade; +truncate company_host cascade; +truncate company_user cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + + +insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, country_code, currency_code, default_lang_tag) +values ( 2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'ES', 'EUR', 'ca') + , ( 4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'FR', 'USD', 'ca') + , ( 6, 'Company 6', 'XX345', '', '777-777-777', 'c@c', '', '', '', '', '', '', 'GB', 'EUR', 'en') + , ( 8, 'Company 8', 'XX456', '', '888-888-888', 'd@d', '', '', '', '', '', '', 'DE', 'USD', 'es') +; + +insert into company_user (company_id, user_id, role) +values (2, 1, 'admin') + , (4, 5, 'admin') +; + +insert into company_host (company_id, host) +values (2, 'co2') + , (4, 'co4') +; + +insert into redsys(company_id, merchant_code, terminal_number, environment, integration, encrypt_key) +values (4, '444444444', 4, 'live', 'insite', E'\\x44') + , (8, '888888888', 8, 'test', 'redirect', E'\\x88') +; + +prepare redsys_data as +select company_id, merchant_code, terminal_number +from redsys +; + +set role guest; +select bag_eq( + 'redsys_data', + $$ values (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'Everyone should be able to list Redsys settings from all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into redsys(company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '222222222', 2, 'live', 'insite', E'\\x22' ) $$, + 'Admin from company 2 should be able to set up their Redsys settings.' +); + +select bag_eq( + 'redsys_data', + $$ values (2, '222222222', 2) + , (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update redsys set merchant_code = '121212121', terminal_number = 1, encrypt_key = E'\\x12' where company_id = 2 $$, + 'Admin from company 2 should be able to update their Redsys settings.' +); + +select bag_eq( + 'redsys_data', + $$ values (2, '121212121', 1) + , (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from redsys where company_id = 2 $$, + 'Admin from company 2 should be able to delete their Redsys settings.' +); + +select bag_eq( + 'redsys_data', + $$ values (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (6, '666666666', 6, 'live', 'insite', E'\x66' ) $$, + '42501', 'new row violates row-level security policy for table "redsys"', + 'Admin from company 2 should NOT be able to setup Redsys for another company.' +); + +select lives_ok( + $$ update redsys set merchant_code = '123456789', terminal_number = 10 where company_id = 4 $$, + 'Admin from company 2 should NOT be able to update the Redsys settings of company 4, but no error' +); + +select bag_eq( + 'redsys_data', + $$ values (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'No row should have been updated.' +); + +select lives_ok( + $$ update redsys set company_id = 2 where company_id = 4 $$, + 'Admin from company 2 should NOT be able to usurp the Redsys settings of company 4, but no error because company_id is primary key' +); + +select lives_ok( + $$ delete from redsys where company_id = 4 $$, + 'Admin from company 2 should NOT be able to delete the Redsys of company 4, but not error is thrown' +); + +select bag_eq( + 'redsys_data', + $$ values (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'No row should have been deleted' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '22222222', 1, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "merchant_code_valid"', + 'Should not be able to insert a merchant code less than 9 digits long.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '2222222222', 1, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "merchant_code_valid"', + 'Should not be able to insert a merchant code more than 9 digits long.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '2222a2222', 1, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "merchant_code_valid"', + 'Should not be able to insert a merchant code that contains a non-digit character.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '222222222', 0, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "terminal_number_in_range"', + 'Should not be able to insert a terminal code smaller than 1.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '222222222', 1000, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "terminal_number_in_range"', + 'Should not be able to insert a terminal code greater than 999.' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/redsys_encrypt.sql b/test/redsys_encrypt.sql new file mode 100644 index 0000000..a2f3b87 --- /dev/null +++ b/test/redsys_encrypt.sql @@ -0,0 +1,49 @@ +-- Test redsys_encrypt +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(15); + +set search_path to camper, public; + +select has_function('camper', 'redsys_encrypt', array['integer', 'bytea']); +select function_lang_is('camper', 'redsys_encrypt', array['integer', 'bytea'], 'sql'); +select function_returns('camper', 'redsys_encrypt', array['integer', 'bytea'], 'bytea'); +select is_definer('camper', 'redsys_encrypt', array['integer', 'bytea']); +select volatility_is('camper', 'redsys_encrypt', array['integer', 'bytea'], 'stable'); +select function_privs_are('camper', 'redsys_encrypt', array ['integer', 'bytea'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'redsys_encrypt', array ['integer', 'bytea'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'redsys_encrypt', array ['integer', 'bytea'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'redsys_encrypt', array ['integer', 'bytea'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate redsys cascade; +truncate company cascade; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) +values (2, '222222222', 1, 'live', 'insite', decode('x8RS+UkDkAN3i5hGU92U2c5BhJlffg8w', 'base64')) + , (4, '999008881', 1, 'test', 'insite', decode('sq7HjrUOBfKmC576ILgskD5srU870gJ7', 'base64')) +; + + +select is(redsys_encrypt(2, '\x61626364656667'::bytea), '\x5df27f7580163beb'::bytea); +select is(redsys_encrypt(2, '\x3030303030303031'::bytea), '\x6cf019b21fc92a68'::bytea); +select is(redsys_encrypt(2, '\x414243444546474849'::bytea), '\x3aa8d51dd4dfa79be2fa13de2c636f06'::bytea); + +select is(redsys_encrypt(4, '\x61626364656667'::bytea), '\x9b9cc420acfcd328'::bytea); +select is(redsys_encrypt(4, '\x3030303030303031'::bytea), '\x8ae3e9826d15ef02'::bytea); +select is(redsys_encrypt(4, '\x414243444546474849'::bytea), '\xce914bbfb35a70c38b7ea38cee8e7aea'::bytea); + + +select * +from finish(); + +rollback; diff --git a/test/redsys_environment.sql b/test/redsys_environment.sql new file mode 100644 index 0000000..89f3644 --- /dev/null +++ b/test/redsys_environment.sql @@ -0,0 +1,18 @@ +-- Test redsys_environment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(2); + +set search_path to camper, public; + +select has_enum('camper', 'redsys_environment', 'Enum redsys_environment should exist'); +select enum_has_labels('camper', 'redsys_environment', array['test', 'live']); + +select * +from finish(); + +rollback; diff --git a/test/redsys_integration.sql b/test/redsys_integration.sql new file mode 100644 index 0000000..6ec2a63 --- /dev/null +++ b/test/redsys_integration.sql @@ -0,0 +1,18 @@ +-- Test redsys_integration +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(2); + +set search_path to camper, public; + +select has_enum('camper', 'redsys_integration', 'Enum type camper.redsys_integration should exist'); +select enum_has_labels('camper', 'redsys_integration', array['insite', 'redirect']); + +select * +from finish(); + +rollback; diff --git a/test/redsys_request.sql b/test/redsys_request.sql new file mode 100644 index 0000000..4fc1c58 --- /dev/null +++ b/test/redsys_request.sql @@ -0,0 +1,28 @@ +-- Test redsys_request +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(11); + +set search_path to camper, public; + +select has_composite('camper', 'redsys_request', 'Composite type camper.redsys_request should exist'); +select columns_are('camper', 'redsys_request', array['transaction_type', 'amount', 'order_number', 'product', 'card_holder', 'success_uri', 'failure_uri', 'notification_uri', 'lang_tag']); +select col_type_is('camper'::name, 'redsys_request'::name, 'transaction_type'::name, 'integer'); +select col_type_is('camper'::name, 'redsys_request'::name, 'amount'::name, 'text'); +select col_type_is('camper'::name, 'redsys_request'::name, 'order_number'::name, 'text'); +select col_type_is('camper'::name, 'redsys_request'::name, 'product'::name, 'text'); +select col_type_is('camper'::name, 'redsys_request'::name, 'card_holder'::name, 'text'); +select col_type_is('camper'::name, 'redsys_request'::name, 'success_uri'::name, 'uri'); +select col_type_is('camper'::name, 'redsys_request'::name, 'failure_uri'::name, 'uri'); +select col_type_is('camper'::name, 'redsys_request'::name, 'notification_uri'::name, 'uri'); +select col_type_is('camper'::name, 'redsys_request'::name, 'lang_tag'::name, 'text'); + + +select * +from finish(); + +rollback; diff --git a/test/redsys_sign_request.sql b/test/redsys_sign_request.sql new file mode 100644 index 0000000..269f8c0 --- /dev/null +++ b/test/redsys_sign_request.sql @@ -0,0 +1,49 @@ +-- Test redsys_sign_request +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(11); + +set search_path to camper, public; + +select has_function('camper', 'redsys_sign_request', array['integer', 'redsys_request']); +select function_lang_is('camper', 'redsys_sign_request', array['integer', 'redsys_request'], 'sql'); +select function_returns('camper', 'redsys_sign_request', array['integer', 'redsys_request'], 'redsys_signed_request'); +select isnt_definer('camper', 'redsys_sign_request', array['integer', 'redsys_request']); +select volatility_is('camper', 'redsys_sign_request', array['integer', 'redsys_request'], 'stable'); +select function_privs_are('camper', 'redsys_sign_request', array ['integer', 'redsys_request'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'redsys_sign_request', array ['integer', 'redsys_request'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'redsys_sign_request', array ['integer', 'redsys_request'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'redsys_sign_request', array ['integer', 'redsys_request'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate redsys cascade; +truncate company cascade; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) +values (2, '222222222', 2, 'live', 'insite', decode('x8RS+UkDkAN3i5hGU92U2c5BhJlffg8w', 'base64')) + , (4, '999008881', 4, 'test', 'insite', decode('sq7HjrUOBfKmC576ILgskD5srU870gJ7', 'base64')) +; + +select is( + redsys_sign_request(2, row(0, '12.34', '00000001', 'Product #2', 'My Name', 'https://example.com/ok', 'https://example.com/ko', 'https://example.com/notification', 'ca')), + row('eyJEc19NZXJjaGFudF9NZXJjaGFudENvZGUiIDogIjIyMjIyMjIyMiIsICJEc19NZXJjaGFudF9UZXJtaW5hbCIgOiAiMiIsICJEc19NZXJjaGFudF9NZXJjaGFudE5hbWUiIDogIkNvbXBhbnkgMiIsICJEc19NZXJjaGFudF9UcmFuc2FjdGlvblR5cGUiIDogIjAiLCAiRHNfTWVyY2hhbnRfQW1vdW50IiA6ICIxMjM0IiwgIkRzX01lcmNoYW50X0N1cnJlbmN5IiA6ICI5NzgiLCAiRHNfTWVyY2hhbnRfT3JkZXIiIDogIjAwMDAwMDAxIiwgIkRzX01lcmNoYW50X1Byb2R1Y3RkZXNjcmlwdGlvbiIgOiAiUHJvZHVjdCAjMiIsICJEc19NZXJjaGFudF9UaXR1bGFyIiA6ICJNeSBOYW1lIiwgIkRzX01lcmNoYW50X1VybE9LIiA6ICJodHRwczovL2V4YW1wbGUuY29tL29rIiwgIkRzX01lcmNoYW50X1VybEtPIiA6ICJodHRwczovL2V4YW1wbGUuY29tL2tvIiwgIkRzX01lcmNoYW50X01lcmNoYW50VVJMIiA6ICJodHRwczovL2V4YW1wbGUuY29tL25vdGlmaWNhdGlvbiIsICJEc19NZXJjaGFudF9Db25zdW1lckxhbmd1YWdlIiA6ICIzIn0=', 'aELGpY6jRl48YXNFkRKuIhhddroJpdeFigsRPDsB3pQ=', 'HMAC_SHA256_V1')::redsys_signed_request +); + +select is( + redsys_sign_request(4, row(1, '2144.00', '0000ABCDE', 'Booking ABCDE', 'Customer Full Name', 'http://example.cat/success', 'http://example.cat/failure', 'http://example.cat/notify', 'en')), + row('eyJEc19NZXJjaGFudF9NZXJjaGFudENvZGUiIDogIjk5OTAwODg4MSIsICJEc19NZXJjaGFudF9UZXJtaW5hbCIgOiAiNCIsICJEc19NZXJjaGFudF9NZXJjaGFudE5hbWUiIDogIkNvbXBhbnkgNCIsICJEc19NZXJjaGFudF9UcmFuc2FjdGlvblR5cGUiIDogIjEiLCAiRHNfTWVyY2hhbnRfQW1vdW50IiA6ICIyMTQ0MDAiLCAiRHNfTWVyY2hhbnRfQ3VycmVuY3kiIDogIjg0MCIsICJEc19NZXJjaGFudF9PcmRlciIgOiAiMDAwMEFCQ0RFIiwgIkRzX01lcmNoYW50X1Byb2R1Y3RkZXNjcmlwdGlvbiIgOiAiQm9va2luZyBBQkNERSIsICJEc19NZXJjaGFudF9UaXR1bGFyIiA6ICJDdXN0b21lciBGdWxsIE5hbWUiLCAiRHNfTWVyY2hhbnRfVXJsT0siIDogImh0dHA6Ly9leGFtcGxlLmNhdC9zdWNjZXNzIiwgIkRzX01lcmNoYW50X1VybEtPIiA6ICJodHRwOi8vZXhhbXBsZS5jYXQvZmFpbHVyZSIsICJEc19NZXJjaGFudF9NZXJjaGFudFVSTCIgOiAiaHR0cDovL2V4YW1wbGUuY2F0L25vdGlmeSIsICJEc19NZXJjaGFudF9Db25zdW1lckxhbmd1YWdlIiA6ICIyIn0=', '5FybTC6lOiTUTeFCGiCZ7LCKb60Jt2CgCBz6QbfB/o0=', 'HMAC_SHA256_V1')::redsys_signed_request +); + +select * +from finish(); + +rollback; diff --git a/test/redsys_signed_request.sql b/test/redsys_signed_request.sql new file mode 100644 index 0000000..b2254ab --- /dev/null +++ b/test/redsys_signed_request.sql @@ -0,0 +1,22 @@ +-- Test redsys_signed_request +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(5); + +set search_path to camper, public; + +select has_composite('camper', 'redsys_signed_request', 'Composite type camper.redsys_signed_request should exist'); +select columns_are('camper', 'redsys_signed_request', array['merchant_parameters', 'signature', 'signature_version']); +select col_type_is('camper'::name, 'redsys_signed_request'::name, 'merchant_parameters'::name, 'text'); +select col_type_is('camper'::name, 'redsys_signed_request'::name, 'signature'::name, 'text'); +select col_type_is('camper'::name, 'redsys_signed_request'::name, 'signature_version'::name, 'text'); + + +select * +from finish(); + +rollback; diff --git a/test/setup_redsys.sql b/test/setup_redsys.sql new file mode 100644 index 0000000..996b36c --- /dev/null +++ b/test/setup_redsys.sql @@ -0,0 +1,78 @@ +-- Test setup_redsys +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(15); + +set search_path to camper, public; + +select has_function('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text']); +select function_lang_is('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'plpgsql'); +select function_returns('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'void'); +select isnt_definer('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text']); +select volatility_is('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'volatile'); +select function_privs_are('camper', 'setup_redsys', array ['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'setup_redsys', array ['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'setup_redsys', array ['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'setup_redsys', array ['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate redsys cascade; +truncate company cascade; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +prepare redsys_data as +select company_id, merchant_code, terminal_number, environment::text, integration::text, encrypt_key +from redsys +; + +select lives_ok( + $$ select setup_redsys(1, '111111111', 1, 'live', 'insite', encode(E'\\x1111', 'base64')) $$, + 'Should be able to setup Redsys parameters for the first company' +); + +select lives_ok( + $$ select setup_redsys(2, '222222222', 2, 'test', 'redirect', encode(E'\\x1212', 'base64')) $$, + 'Should be able to setup Redsys parameters for the second company' +); + +select bag_eq( + 'redsys_data', + $$ values (1, '111111111', 1, 'live', 'insite', E'\\x1111'::bytea), + (2, '222222222', 2, 'test', 'redirect', E'\\x1212'::bytea) + $$, + 'Should have inserted all Redsys parameters' +); + +select lives_ok( + $$ select setup_redsys(1, '999999999', 9, 'live', 'redirect', encode(E'\\x9119', 'base64')) $$, + 'Should be able to update Redsys parameters for the first company' +); + +select lives_ok( + $$ select setup_redsys(2, '123456789', 5, 'live', 'redirect', NULL) $$, + 'Should be able to update Redsys parameters for the second company, leaving the encrypt key alone' +); + +select bag_eq( + 'redsys_data', + $$ values (1, '999999999', 9, 'live', 'redirect', E'\\x9119'::bytea), + (2, '123456789', 5, 'live', 'redirect', E'\\x1212'::bytea) + $$, + 'Should have updated all Redsys parameters' +); + + +reset client_min_messages; +select * +from finish(); + +rollback; diff --git a/test/zero_pad.sql b/test/zero_pad.sql new file mode 100644 index 0000000..1dd853b --- /dev/null +++ b/test/zero_pad.sql @@ -0,0 +1,36 @@ +-- Test zero_pad +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(19); + +set search_path to camper, public; + +select has_function('camper', 'zero_pad', array['bytea', 'integer']); +select function_lang_is('camper', 'zero_pad', array['bytea', 'integer'], 'sql'); +select function_returns('camper', 'zero_pad', array['bytea', 'integer'], 'bytea'); +select isnt_definer('camper', 'zero_pad', array['bytea', 'integer']); +select volatility_is('camper', 'zero_pad', array['bytea', 'integer'], 'immutable'); +select function_privs_are('camper', 'zero_pad', array ['bytea', 'integer'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'zero_pad', array ['bytea', 'integer'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'zero_pad', array ['bytea', 'integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'zero_pad', array ['bytea', 'integer'], 'authenticator', array[]::text[]); + +select is(zero_pad(''::bytea, 8), '\x'::bytea); +select is(zero_pad('\x11'::bytea, 8), '\x1100000000000000'::bytea); +select is(zero_pad('\x2211'::bytea, 8), '\x2211000000000000'::bytea); +select is(zero_pad('\x332211'::bytea, 8), '\x3322110000000000'::bytea); +select is(zero_pad('\x44332211'::bytea, 8), '\x4433221100000000'::bytea); +select is(zero_pad('\x5544332211'::bytea, 8), '\x5544332211000000'::bytea); +select is(zero_pad('\x665544332211'::bytea, 8), '\x6655443322110000'::bytea); +select is(zero_pad('\x77665544332211'::bytea, 8), '\x7766554433221100'::bytea); +select is(zero_pad('\x8877665544332211'::bytea, 8), '\x8877665544332211'::bytea); +select is(zero_pad('\x998877665544332211'::bytea, 8), '\x99887766554433221100000000000000'::bytea); + +select * +from finish(); + +rollback; diff --git a/verify/available_currencies.sql b/verify/available_currencies.sql index f308500..252e846 100644 --- a/verify/available_currencies.sql +++ b/verify/available_currencies.sql @@ -9,6 +9,7 @@ from currency where currency_code = 'EUR' and currency_symbol = '€' and decimal_digits = 2 + and redsys_code = 978 ; select 1 / count(*) @@ -16,6 +17,7 @@ from currency where currency_code = 'USD' and currency_symbol = '$' and decimal_digits = 2 + and redsys_code = 840 ; rollback; diff --git a/verify/available_languages.sql b/verify/available_languages.sql index 764a539..c38450e 100644 --- a/verify/available_languages.sql +++ b/verify/available_languages.sql @@ -10,7 +10,8 @@ where lang_tag = 'und' and name = 'Undefined' and endonym = 'Undefined' and not selectable - and currency_pattern = '%[3]s%.[1]*[2]f'; + and currency_pattern = '%[3]s%.[1]*[2]f' + and redsys_code = 0; select 1 / count(*) from language @@ -18,7 +19,8 @@ where lang_tag = 'ca' and name = 'Catalan' and endonym = 'català' and selectable - and currency_pattern = '%.[1]*[2]f %[3]s'; + and currency_pattern = '%.[1]*[2]f %[3]s' + and redsys_code = 3; select 1 / count(*) from language @@ -26,7 +28,8 @@ where lang_tag = 'en' and name = 'English' and endonym = 'English' and selectable - and currency_pattern = '%[3]s%.[1]*[2]f'; + and currency_pattern = '%[3]s%.[1]*[2]f' + and redsys_code = 2; select 1 / count(*) from language @@ -34,6 +37,7 @@ where lang_tag = 'es' and name = 'Spanish' and endonym = 'español' and selectable - and currency_pattern = '%.[1]*[2]f %[3]s'; + and currency_pattern = '%.[1]*[2]f %[3]s' + and redsys_code = 1; rollback; diff --git a/verify/currency.sql b/verify/currency.sql index 6b89c9a..6f72fc5 100644 --- a/verify/currency.sql +++ b/verify/currency.sql @@ -5,6 +5,7 @@ begin; select currency_code , currency_symbol , decimal_digits + , redsys_code from camper.currency where false; diff --git a/verify/encode_base64url.sql b/verify/encode_base64url.sql new file mode 100644 index 0000000..b642745 --- /dev/null +++ b/verify/encode_base64url.sql @@ -0,0 +1,7 @@ +-- Verify camper:encode_base64url on pg + +begin; + +select has_function_privilege('camper.encode_base64url(bytea)', 'execute'); + +rollback; diff --git a/verify/language.sql b/verify/language.sql index 2a74912..025f18c 100644 --- a/verify/language.sql +++ b/verify/language.sql @@ -7,6 +7,7 @@ select lang_tag , endonym , selectable , currency_pattern + , redsys_code from public.language where false; diff --git a/verify/redsys.sql b/verify/redsys.sql new file mode 100644 index 0000000..fc1b272 --- /dev/null +++ b/verify/redsys.sql @@ -0,0 +1,20 @@ +-- Verify camper:redsys on pg + +begin; + +select company_id + , merchant_code + , terminal_number + , environment + , integration + , encrypt_key +from camper.redsys +where false; + +select 1 / count(*) from pg_class where oid = 'camper.redsys'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.redsys'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.redsys'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.redsys'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.redsys'::regclass; + +rollback; diff --git a/verify/redsys_encrypt.sql b/verify/redsys_encrypt.sql new file mode 100644 index 0000000..8ee31a1 --- /dev/null +++ b/verify/redsys_encrypt.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_encrypt on pg + +begin; + +select has_function_privilege('camper.redsys_encrypt(integer, bytea)', 'execute'); + +rollback; diff --git a/verify/redsys_environment.sql b/verify/redsys_environment.sql new file mode 100644 index 0000000..81d7aa5 --- /dev/null +++ b/verify/redsys_environment.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_environment on pg + +begin; + +select has_type_privilege('camper.redsys_environment', 'usage'); + +rollback; diff --git a/verify/redsys_integration.sql b/verify/redsys_integration.sql new file mode 100644 index 0000000..b7d0aca --- /dev/null +++ b/verify/redsys_integration.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_integration on pg + +begin; + +select pg_catalog.has_type_privilege('camper.redsys_integration', 'usage'); + +rollback; diff --git a/verify/redsys_request.sql b/verify/redsys_request.sql new file mode 100644 index 0000000..e2f0245 --- /dev/null +++ b/verify/redsys_request.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_request on pg + +begin; + +select pg_catalog.has_type_privilege('camper.redsys_request', 'usage'); + +rollback; diff --git a/verify/redsys_sign_request.sql b/verify/redsys_sign_request.sql new file mode 100644 index 0000000..7eb4dfd --- /dev/null +++ b/verify/redsys_sign_request.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_sign_request on pg + +begin; + +select has_function_privilege('camper.redsys_sign_request(integer, camper.redsys_request)', 'execute'); + +rollback; diff --git a/verify/redsys_signed_request.sql b/verify/redsys_signed_request.sql new file mode 100644 index 0000000..86e8802 --- /dev/null +++ b/verify/redsys_signed_request.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_signed_request on pg + +begin; + +select pg_catalog.has_type_privilege('camper.redsys_signed_request', 'usage'); + +rollback; diff --git a/verify/setup_redsys.sql b/verify/setup_redsys.sql new file mode 100644 index 0000000..5b18830 --- /dev/null +++ b/verify/setup_redsys.sql @@ -0,0 +1,7 @@ +-- Verify camper:setup_redsys on pg + +begin; + +select has_function_privilege('camper.setup_redsys(integer, text, integer, camper.redsys_environment, camper.redsys_integration, text)', 'execute'); + +rollback; diff --git a/verify/zero_pad.sql b/verify/zero_pad.sql new file mode 100644 index 0000000..2dcd2d9 --- /dev/null +++ b/verify/zero_pad.sql @@ -0,0 +1,7 @@ +-- Verify camper:zero_pad on pg + +begin; + +select has_function_privilege('camper.zero_pad(bytea, integer)', 'execute'); + +rollback;