feat(flake): Add unit testing

Change-Id: Idc9bbca0e752b21a5293e2ad4819da9af42ca8ca
Reviewed-on: https://git.clicks.codes/c/Infra/NixFiles/+/782
Tested-by: Skyler Grey <minion@clicks.codes>
Reviewed-by: Samuel Shuert <coded@clicks.codes>
diff --git a/flake.lock b/flake.lock
index 71d0a65..e54a23e 100644
--- a/flake.lock
+++ b/flake.lock
@@ -140,16 +140,15 @@
         ]
       },
       "locked": {
-        "lastModified": 1713814392,
-        "narHash": "sha256-IanrgtpgDqxGfzNczstspPljAHKaY0e4DGvYgdAwC1Y=",
+        "lastModified": 1719005984,
+        "narHash": "sha256-mpFl3Jv4fKnn+5znYXG6SsBjfXHJdRG5FEqNSPx0GLA=",
         "owner": "snowfallorg",
         "repo": "lib",
-        "rev": "91ab40c2e01cc1bade8092604370964ee86e9317",
+        "rev": "c6238c83de101729c5de3a29586ba166a9a65622",
         "type": "github"
       },
       "original": {
         "owner": "snowfallorg",
-        "ref": "dev",
         "repo": "lib",
         "type": "github"
       }
diff --git a/flake.nix b/flake.nix
index da9ce5a..00c6592 100644
--- a/flake.nix
+++ b/flake.nix
@@ -10,16 +10,17 @@
     nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
     unstable.url = "github:nixos/nixpkgs/nixos-unstable";
 
-    snowfall-lib = {
-      url = "github:snowfallorg/lib/dev";
-      inputs.nixpkgs.follows = "nixpkgs";
-    };
 
     deploy-rs = {
       url = "github:serokell/deploy-rs";
       inputs.nixpkgs.follows = "nixpkgs";
     };
 
+    snowfall-lib = {
+      url = "github:snowfallorg/lib";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
+
     sops-nix = {
       url = "github:Mic92/sops-nix";
       inputs = {
@@ -34,7 +35,7 @@
   outputs =
     inputs:
     let
-      lib = inputs.snowfall-lib.mkLib {
+      lib = (inputs.snowfall-lib.mkLib {
         inherit inputs;
 
         src = ./.;
@@ -42,7 +43,7 @@
         snowfall = {
           namespace = "clicks";
         };
-      };
+      }).snowfall.internal.system-lib;
     in
     lib.mkFlake {
       overlays = with inputs; [ ];
@@ -52,17 +53,42 @@
         inputs.sops-nix.nixosModules.sops
       ];
 
-      deploy = lib.deploy.mkDeploy {
+      deploy = lib.clicks.deploy.mkDeploy {
         inherit (inputs) self;
         overrides = {
           teal.hostname = "teal.alpha.clicks.domains";
         };
       };
 
-      checks = builtins.mapAttrs (
-        system: deploy-lib: deploy-lib.deployChecks inputs.self.deploy
-      ) inputs.deploy-rs.lib;
+      outputs-builder = channels: {
+        specs = let
+          nixFiles = lib.snowfall.fs.get-nix-files-recursive ./.;
+          specFiles = builtins.filter (lib.clicks.strings.endsWith ".spec.nix") nixFiles;
+          importedSpecs = lib.forEach specFiles (file: {
+            name = builtins.unsafeDiscardStringContext (builtins.baseNameOf file);
+            value = import file {
+              inherit channels lib;
+            };
+          });
+        in builtins.listToAttrs importedSpecs;
 
-      outputs-builder = channels: { formatter = channels.nixpkgs.nixfmt-rfc-style; };
+        formatter = channels.nixpkgs.nixfmt-rfc-style;
+
+        checks = let
+          allChecks = {
+            deploy-rs = lib.deploy-rs.${channels.nixpkgs.system}.deployChecks inputs.self.deploy;
+            clicks = lib.clicks.checks channels.nixpkgs;
+          };
+
+          mergedChecks = lib.trivial.pipe allChecks [
+            (lib.attrsets.mapAttrsToList (sourceName: (lib.attrsets.mapAttrsToList (checkName: value: {
+              name = "${sourceName}:${checkName}";
+              inherit value;
+            }))))
+            lib.lists.flatten
+            builtins.listToAttrs
+          ];
+        in mergedChecks;
+      };
     };
 }
diff --git a/lib/checks/default.nix b/lib/checks/default.nix
new file mode 100644
index 0000000..0340be1
--- /dev/null
+++ b/lib/checks/default.nix
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{
+  lib,
+  inputs,
+  ...
+}: {
+  checks = pkgs: {
+    nix-unit = pkgs.stdenv.mkDerivation {
+      name = "run nix-unit";
+      src = ./../..;
+      buildPhase = let
+        getSubInputs = prefix: inputs: lib.trivial.pipe inputs [
+          (lib.attrsets.mapAttrsToList (name: value: [
+            "--override-input ${prefix}${name} ${value.outPath}"
+            (if value ? inputs
+             then getSubInputs "${prefix}${name}/" value.inputs
+             else [])
+          ]))
+          lib.lists.flatten
+          (builtins.concatStringsSep " ")
+        ];
+        inputPathArgs = getSubInputs "" inputs;
+      in ''
+        export HOME="$(realpath .)"
+        ${pkgs.lib.getExe pkgs.nix-unit} \
+          --eval-store $HOME \
+          --flake \
+          --option extra-experimental-features flakes \
+          ${inputPathArgs} \
+          .#specs.${pkgs.system}
+        touch $out
+      '';
+    };
+  };
+}
diff --git a/lib/strings/default.nix b/lib/strings/default.nix
index dccb09f..ca056e0 100644
--- a/lib/strings/default.nix
+++ b/lib/strings/default.nix
@@ -11,5 +11,14 @@
         prefixLength = lib.strings.commonPrefixLength a b;
       in
       builtins.substring 0 prefixLength a;
+
+    endsWith = suffix: str: let
+      suffixLength = builtins.stringLength suffix;
+      strLength = builtins.stringLength str;
+
+      suffixStart = strLength - suffixLength;
+
+      maybeSuffix = builtins.substring suffixStart strLength str;
+    in suffixStart >= 0 && maybeSuffix == suffix;
   };
 }
diff --git a/shells/default/default.nix b/shells/default/default.nix
index d7d85e1..d453305 100644
--- a/shells/default/default.nix
+++ b/shells/default/default.nix
@@ -5,6 +5,7 @@
 
 {
   mkShell,
+  nix-unit,
   reuse,
   deploy-rs,
 }:
@@ -12,5 +13,6 @@
   packages = [
     reuse # Used to provide licenses & copyright attribution
     deploy-rs # Used to deploy to our servers
+    nix-unit # Used to do unit testing
   ];
 }