Skip to content

Commit

Permalink
feat: add register-endpoint to cns_root, extend tests
Browse files Browse the repository at this point in the history
  • Loading branch information
przydatek committed Jan 16, 2025
1 parent 02f93a2 commit 641624f
Show file tree
Hide file tree
Showing 6 changed files with 383 additions and 100 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,16 @@ jobs:
run: dfx start --background

- name: 'Test cns_root canister'
# NOTE: The functionality of cns_root depends on whehter the caller is a controller or not,
# so we proceed in two steps. Initially the test canister (i.e. cns_root) is not a controller,
# so we test the behaviour for non-controller callers. Afterwards we make the test canister
# a controller of cns_root, and test the behaviour for controller callers.
run: |
dfx deploy --no-wallet cns_root_test
echo "Calling runTestsIfNotController on canister cns_root_test ..."
dfx canister call cns_root_test runTestsIfNotController "()"
echo "Adding cns_root_test-canister as a controller of cns_root-canister..."
dfx canister update-settings cns_root --add-controller `dfx canister id cns_root_test`
echo "Calling runTests on canister cns_root_test..."
dfx canister call cns_root_test runTests "()"
Expand Down
107 changes: 89 additions & 18 deletions minimal_cns/src/backend/cns_root.mo
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
import NameRegistry "canister:name_registry";
import Iter "mo:base/Iter";
import Map "mo:base/OrderedMap";
import Option "mo:base/Option";
import Principal "mo:base/Principal";
import Text "mo:base/Text";
import Types "cns_types";

shared actor class () {
let icpTld = ".icp";
let icpTldCanisterId = "qoctq-giaaa-aaaaa-aaaea-cai";

public shared func lookup(domain : Text, recordType : Text) : async NameRegistry.DomainLookup {
var answers : [NameRegistry.DomainRecord] = [];
var authorities : [NameRegistry.DomainRecord] = [];
type DomainRecordsMap = Map.Map<Text, Types.DomainRecord>;
let answersWrapper = Map.Make<Text>(Text.compare);
stable var lookupAnswersMap : DomainRecordsMap = answersWrapper.empty();
stable var lookupAuthoritiesMap : DomainRecordsMap = answersWrapper.empty();

if (Text.endsWith(Text.toLowercase(domain), #text icpTld)) {
func getTld(domain : Text) : Text {
let parts = Text.split(domain, #char '.');
let array = Iter.toArray(parts);
let lastPart = array[array.size() - 1];
return "." # lastPart;
};

public shared func lookup(domain : Text, recordType : Text) : async Types.DomainLookup {
var answers : [Types.DomainRecord] = [];
var authorities : [Types.DomainRecord] = [];

let domainLowercase : Text = Text.toLowercase(domain);
if (Text.endsWith(domainLowercase, #text icpTld)) {
let tld = getTld(domainLowercase);
switch (Text.toUppercase(recordType)) {
case ("NC") {
answers := [{
name = ".icp.";
record_type = "NC";
ttl = 3600;
data = icpTldCanisterId;
}];
let maybeRecord : ?Types.DomainRecord = answersWrapper.get(lookupAnswersMap, tld);
answers := switch maybeRecord {
case null { [] };
case (?record) { [record] };
};
};
case _ {
authorities := [{
name = ".icp.";
record_type = "NC";
ttl = 3600;
data = icpTldCanisterId;
}];
let maybeRecord : ?Types.DomainRecord = answersWrapper.get(lookupAuthoritiesMap, tld);
authorities := switch maybeRecord {
case null { [] };
case (?record) { [record] };
};
};
};
};
Expand All @@ -36,4 +51,60 @@ shared actor class () {
authorities = authorities;
};
};

public shared ({ caller }) func register(domain : Text, records : Types.RegistrationRecords) : async (Types.RegisterResult) {
if (not Principal.isController(caller)) {
return {
success = false;
message = ?("Currently only a canister controller can register new TLD-operators, caller: " # Principal.toText(caller));
};
};
let domainLowercase : Text = Text.toLowercase(domain);
let tld = getTld(domainLowercase);
if (tld != domainLowercase) {
return {
success = false;
message = ?("The given domain " # domain # "is not a TLD, its TLD is " # tld);
};
};
if (tld != icpTld) {
return {
success = false;
message = ?("Currently only " # icpTld # "-TLD is supported; requested TLD: " # domain);
};
};
let domainRecords = Option.get(records.records, []);
// TODO: remove the restriction of acceping exactly one domain record.
if (domainRecords.size() != 1) {
return {
success = false;
message = ?"Currently exactly one domain record must be specified.";
};
};
let record : Types.DomainRecord = domainRecords[0];
if (tld # "." != (Text.toLowercase(record.name))) {
return {
success = false;
message = ?("Inconsistent domain record, record.name: `" # record.name # "` doesn't match TLD: " # tld);
};
};
// TODO: add more checks: validate domain name and all the fields of the domain record(s).

switch (Text.toUppercase(record.record_type)) {
case ("NC") {
lookupAnswersMap := answersWrapper.put(lookupAnswersMap, tld, record);
lookupAuthoritiesMap := answersWrapper.put(lookupAuthoritiesMap, tld, record);
return {
success = true;
message = null;
};
};
case _ {
return {
success = false;
message = ?("Unsupported record_type: `" # record.record_type # "`, expected 'NC'");
};
};
};
};
};
194 changes: 184 additions & 10 deletions minimal_cns/src/backend/cns_root.test.mo
Original file line number Diff line number Diff line change
@@ -1,17 +1,59 @@
import CnsRoot "canister:cns_root";
import Nat32 "mo:base/Nat32";
import Test "../test_utils"
import Option "mo:base/Option";
import Test "../test_utils";
import Types "cns_types";

actor {
public func runTests() : async () {
await shouldGetIcpTldOperatorForNcIcpLookups();
await shouldGetIcpTldOperatorForOtherIcpLookups();
// The order of tests matters.
await shouldNotGetIcpTldOperatorBeforeRegistration();
await shouldRegisterIcpTldOperator();
await shouldGetIcpTldOperatorForNcIcpLookupsAfterRegistration();
await shouldGetIcpTldOperatorForOtherIcpLookupsAfterRegistration();
// shouldRegisterAndLookupIcpTld() overwrites the previously registered operator.
await shouldRegisterAndLookupIcpTld();
await shouldNotGetOtherTldOperator();
await shouldNotRegisterTldIfDomainNotTld();
await shouldNotRegisterTldIfNotDotIcp();
await shouldNotRegisterTldIfMissingDomainRecord();
await shouldNotRegisterTldIfMultipleDomainRecords();
};

let icpTldCanisterId = "qoctq-giaaa-aaaaa-aaaea-cai";
public func runTestsIfNotController() : async () {
await shouldNotRegisterTldIfNotController();
};

func asText(maybeText : ?Text) : Text {
return Option.get(maybeText, "");
};

let dummyIcpTldCanisterId = "qoctq-giaaa-aaaaa-aaaea-cai";

func shouldNotGetIcpTldOperatorBeforeRegistration() : async () {
let response = await CnsRoot.lookup(".icp", "NC");
let errMsg = "shouldNotGetIcpTldOperatorBeforeRegistration() failed checking the size of response.";
assert Test.isEqualInt(response.answers.size(), 0, errMsg # "answers");
assert Test.isEqualInt(response.additionals.size(), 0, errMsg # "additionals");
assert Test.isEqualInt(response.authorities.size(), 0, errMsg # "authorities");
};

func shouldGetIcpTldOperatorForNcIcpLookups() : async () {
func shouldRegisterIcpTldOperator() : async () {
let domainRecord : Types.DomainRecord = {
name = ".icp.";
record_type = "NC";
ttl = 3600;
data = dummyIcpTldCanisterId;
};
let registrationRecords = {
controller = [];
records = ?[domainRecord];
};
let registerResponse = await CnsRoot.register(".icp", registrationRecords);
assert Test.isTrue(registerResponse.success, asText(registerResponse.message));
};

func shouldGetIcpTldOperatorForNcIcpLookupsAfterRegistration() : async () {
for (
(domain, recordType) in [
(".icp", "NC"),
Expand All @@ -21,19 +63,19 @@ actor {
].vals()
) {
let response = await CnsRoot.lookup(domain, recordType);
let errMsg = "shouldGetIcpTldOperatorForNcIcpLookups() failed for domain: " # domain # ", recordType: " # recordType # "; ";
let errMsg = "shouldGetIcpTldOperatorForNcIcpLookupsAfterRegistration() failed for domain: " # domain # ", recordType: " # recordType # "; ";
assert Test.isEqualInt(response.answers.size(), 1, errMsg # "size of response.answers");
assert Test.isEqualInt(response.additionals.size(), 0, errMsg # "size of response.additionals");
assert Test.isEqualInt(response.authorities.size(), 0, errMsg # "size of response.authorities");
let domainRecord = response.answers[0];
assert Test.isEqualText(domainRecord.name, ".icp.", errMsg # "field: DomainRecord.name");
assert Test.isEqualText(domainRecord.record_type, "NC", errMsg # "field: DomainRecord.record_type");
assert Test.isEqualInt(Nat32.toNat(domainRecord.ttl), 3600, errMsg # "field: DomainRecord.ttl");
assert Test.isEqualText(domainRecord.data, icpTldCanisterId, errMsg # "field: DomainRecord.data");
assert Test.isEqualText(domainRecord.data, dummyIcpTldCanisterId, errMsg # "field: DomainRecord.data");
};
};

func shouldGetIcpTldOperatorForOtherIcpLookups() : async () {
func shouldGetIcpTldOperatorForOtherIcpLookupsAfterRegistration() : async () {
for (
(domain, recordType) in [
(".icp", "CID"),
Expand All @@ -45,15 +87,15 @@ actor {
].vals()
) {
let response = await CnsRoot.lookup(domain, recordType);
let errMsg = "shouldGetIcpTldOperatorForOtherIcpLookups() failed for domain: " # domain # ", recordType: " # recordType # "; ";
let errMsg = "shouldGetIcpTldOperatorForOtherIcpLookupsAfterRegistration() failed for domain: " # domain # ", recordType: " # recordType # "; ";
assert Test.isEqualInt(response.answers.size(), 0, errMsg # "size of response.answers");
assert Test.isEqualInt(response.additionals.size(), 0, errMsg # "size of response.additionals");
assert Test.isEqualInt(response.authorities.size(), 1, errMsg # "size of response.authorities");
let domainRecord = response.authorities[0];
assert Test.isEqualText(domainRecord.name, ".icp.", errMsg # "field: DomainRecord.name");
assert Test.isEqualText(domainRecord.record_type, "NC", errMsg # "field: DomainRecord.record_type");
assert Test.isEqualInt(Nat32.toNat(domainRecord.ttl), 3600, errMsg # "field: DomainRecord.ttl");
assert Test.isEqualText(domainRecord.data, icpTldCanisterId, errMsg # "field: DomainRecord.data");
assert Test.isEqualText(domainRecord.data, dummyIcpTldCanisterId, errMsg # "field: DomainRecord.data");
};
};

Expand All @@ -75,4 +117,136 @@ actor {
};
};

func shouldRegisterAndLookupIcpTld() : async () {
for (
(tld, recordType) in [
(".icp", "NC"),
(".ICP", "Nc"),
(".iCP", "nC"),
(".Icp", "nc"),
].vals()
) {
let someData = "canister-id-for-" # tld # "-" # recordType;
let domainRecord : Types.DomainRecord = {
name = tld # ".";
record_type = recordType;
ttl = 3600;
data = someData;
};
let registrationRecords = {
controller = [];
records = ?[domainRecord];
};
let registerResponse = await CnsRoot.register(tld, registrationRecords);
assert Test.isTrue(registerResponse.success, asText(registerResponse.message));

let lookupResponse = await CnsRoot.lookup(tld, recordType);
let errMsg = "shouldRegisterAndLookupIcpTld() failed for TLD: " # tld # ", recordType: " # recordType # ", size of response.";
assert Test.isEqualInt(lookupResponse.answers.size(), 1, errMsg # "answers");
assert Test.isEqualInt(lookupResponse.additionals.size(), 0, errMsg # "additionals");
assert Test.isEqualInt(lookupResponse.authorities.size(), 0, errMsg # "authorities");

let responseDomainRecord = lookupResponse.answers[0];
assert (responseDomainRecord == domainRecord);
};
};

func shouldNotRegisterTldIfNotController() : async () {
for (
(tld) in [
(".icp"),
(".com"),
("my_domain.icp"),
("example.org"),
].vals()
) {
let domainRecord : Types.DomainRecord = {
name = tld # ".";
record_type = "NC";
ttl = 3600;
data = "aaa-aaaa";
};
let registrationRecords = {
controller = [];
records = ?[domainRecord];
};
let response = await CnsRoot.register(tld, registrationRecords);
let errMsg = "shouldNotRegisterTldIfNotController() failed for domain: " # tld;
assert Test.isTrue(not response.success, errMsg);
assert Test.textContains(asText(response.message), "only a canister controller can register", errMsg);
};
};

func shouldNotRegisterTldIfDomainNotTld() : async () {
for (
(domain) in [
("example.icp"),
("longer.domain.com"),
].vals()
) {
let domainRecord : Types.DomainRecord = {
name = domain # ".";
record_type = "NC";
ttl = 3600;
data = "aaa-aaaa";
};
let registrationRecords = {
controller = [];
records = ?[domainRecord];
};
let response = await CnsRoot.register(domain, registrationRecords);
let errMsg = "shouldNotRegisterTldIfDomainNotTld() failed for domain: " # domain;
assert Test.isTrue(not response.success, errMsg);
assert Test.textContains(asText(response.message), "is not a TLD", errMsg);
};
};

func shouldNotRegisterTldIfNotDotIcp() : async () {
for (
(tld) in [
(".fun"),
(".com"),
(".org"),
].vals()
) {
let domainRecord : Types.DomainRecord = {
name = tld # ".";
record_type = "NC";
ttl = 3600;
data = "aaa-aaaa";
};
let registrationRecords = {
controller = [];
records = ?[domainRecord];
};
let response = await CnsRoot.register(tld, registrationRecords);
let errMsg = "shouldNotRegisterTldIfNotDotIcp() failed for TLD: " # tld;
assert Test.isTrue(not response.success, errMsg);
assert Test.textContains(asText(response.message), "only .icp-TLD is supported", errMsg);
};
};

func shouldNotRegisterTldIfMissingDomainRecord() : async () {
let response = await CnsRoot.register(".icp", { controller = []; records = null });
let errMsg = "shouldNotRegisterTldIfMissingDomainRecord() failed";
assert Test.isTrue(not response.success, errMsg);
assert Test.textContains(asText(response.message), "exactly one domain record", errMsg);
};

func shouldNotRegisterTldIfMultipleDomainRecords() : async () {
let domainRecord : Types.DomainRecord = {
name = ".icp.";
record_type = "NC";
ttl = 3600;
data = "aaa-aaaa";
};
let registrationRecords = {
controller = [];
records = ?[domainRecord, domainRecord];
};
let response = await CnsRoot.register(".icp", registrationRecords);
let errMsg = "shouldNotRegisterTldIfMultipleDomainRecords() failed for two DomainRecords";
assert Test.isTrue(not response.success, errMsg);
assert Test.textContains(asText(response.message), "exactly one domain record", errMsg);
};
};
Loading

0 comments on commit 641624f

Please sign in to comment.